Spaces:
Build error
Build error
| name: GraphQL and OpenAPI Breaking Changes Detection | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, edited] | |
| branches: | |
| - main | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| env: | |
| MAIN_SERVER_PORT: 3000 | |
| CURRENT_SERVER_PORT: 3002 | |
| permissions: | |
| contents: read | |
| jobs: | |
| changed-files-check: | |
| uses: ./.github/workflows/changed-files.yaml | |
| with: | |
| files: | | |
| package.json | |
| packages/twenty-server/** | |
| packages/twenty-emails/** | |
| packages/twenty-shared/** | |
| .github/workflows/ci-breaking-changes.yaml | |
| api-breaking-changes: | |
| needs: changed-files-check | |
| if: needs.changed-files-check.outputs.any_changed == 'true' | |
| timeout-minutes: 45 | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:18 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| redis: | |
| image: redis | |
| ports: | |
| - 6379:6379 | |
| clickhouse: | |
| image: clickhouse/clickhouse-server:25.8.8 | |
| env: | |
| CLICKHOUSE_PASSWORD: clickhousePassword | |
| CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty" | |
| ports: | |
| - 8123:8123 | |
| - 9000:9000 | |
| options: >- | |
| --health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout current branch | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| fetch-depth: 10 | |
| - name: Try to merge main into current branch | |
| id: merge_attempt | |
| run: | | |
| echo "Attempting to merge main into current branch..." | |
| git config user.email "ci@twenty.com" | |
| git config user.name "CI" | |
| git fetch origin main | |
| CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) | |
| echo "Current branch: $CURRENT_BRANCH" | |
| if git merge origin/main --no-edit; then | |
| echo "β Successfully merged main into current branch" | |
| echo "merged=true" >> $GITHUB_OUTPUT | |
| echo "BRANCH_STATE=merged" >> $GITHUB_ENV | |
| else | |
| echo "β Merge failed due to conflicts" | |
| echo "β οΈ Falling back to comparing current branch against main without merge" | |
| # Abort the failed merge (may not exist if merge never started) | |
| git merge --abort 2>/dev/null || true | |
| echo "merged=false" >> $GITHUB_OUTPUT | |
| echo "BRANCH_STATE=conflicts" >> $GITHUB_ENV | |
| fi | |
| - name: Install dependencies | |
| uses: ./.github/actions/yarn-install | |
| - name: Build shared dependencies | |
| run: | | |
| npx nx build twenty-shared | |
| npx nx build twenty-emails | |
| - name: Build current branch server | |
| run: npx nx build twenty-server | |
| - name: Setup databases | |
| run: | | |
| PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "current_branch";' | |
| PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "main_branch";' | |
| - name: Run ClickHouse migrations | |
| run: npx nx clickhouse:migrate twenty-server | |
| env: | |
| CLICKHOUSE_URL: http://default:clickhousePassword@localhost:8123/twenty | |
| CLICKHOUSE_PASSWORD: clickhousePassword | |
| - name: Setup current branch database | |
| run: | | |
| npx nx reset:env twenty-server | |
| set_env_var() { | |
| local var_name="$1" | |
| local var_value="$2" | |
| local env_file="packages/twenty-server/.env" | |
| echo "" >> "$env_file" | |
| if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then | |
| sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file" | |
| else | |
| echo "${var_name}=${var_value}" >> "$env_file" | |
| fi | |
| } | |
| set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/current_branch" | |
| set_env_var "NODE_PORT" "${{ env.CURRENT_SERVER_PORT }}" | |
| set_env_var "REDIS_URL" "redis://localhost:6379" | |
| set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty" | |
| set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword" | |
| npx nx run twenty-server:database:init:prod | |
| - name: Flush cache before seeding current branch | |
| run: npx nx command-no-deps twenty-server -- cache:flush | |
| - name: Seed current branch database with test data | |
| run: | | |
| npx nx command-no-deps twenty-server -- workspace:seed:dev | |
| - name: Start current branch server in background | |
| run: | | |
| echo "=== Current branch .env file contents ===" | |
| cat packages/twenty-server/.env | |
| echo "=== Starting current branch server ===" | |
| nohup npx nx run twenty-server:start:prod > /tmp/current-server.log 2>&1 & | |
| echo $! > /tmp/current-server.pid | |
| echo "Current server PID: $(cat /tmp/current-server.pid)" | |
| - name: Wait for current branch server to be ready | |
| run: | | |
| echo "Waiting for current branch server to start..." | |
| timeout=60 | |
| interval=5 | |
| elapsed=0 | |
| ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json) | |
| while [ $elapsed -lt $timeout ]; do | |
| GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}') | |
| if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \ | |
| curl -fsS "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then | |
| echo "Current branch server is ready!" | |
| break | |
| fi | |
| echo "Current branch server not ready yet, waiting ${interval}s..." | |
| sleep $interval | |
| elapsed=$((elapsed + interval)) | |
| done | |
| if [ $elapsed -ge $timeout ]; then | |
| echo "β Timed out waiting for current branch server to serve a valid schema." | |
| echo "Current server log:" | |
| cat /tmp/current-server.log || echo "No current server log found" | |
| exit 1 | |
| fi | |
| - name: Download GraphQL and REST responses from current branch | |
| run: | | |
| # Read admin token from shared test tokens file (single source of truth) | |
| ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json) | |
| # Load introspection query from file | |
| INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql) | |
| # Prepare the query payload | |
| QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g') | |
| echo "Downloading GraphQL schema from current server..." | |
| curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -d "{\"query\":\"${QUERY_PAYLOAD}\"}" \ | |
| -o current-schema-introspection.json \ | |
| -w "HTTP Status: %{http_code}\n" \ | |
| -s | |
| echo "Downloading GraphQL metadata schema from current server..." | |
| curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/metadata" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -d "{\"query\":\"${QUERY_PAYLOAD}\"}" \ | |
| -o current-metadata-schema-introspection.json \ | |
| -w "HTTP Status: %{http_code}\n" \ | |
| -s | |
| # Download current branch OpenAPI specs | |
| echo "Downloading OpenAPI specifications from current server..." | |
| curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -o current-rest-api.json \ | |
| -w "HTTP Status: %{http_code}\n" | |
| curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/metadata" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -o current-rest-metadata-api.json \ | |
| -w "HTTP Status: %{http_code}\n" | |
| # Verify the downloads | |
| echo "Current branch files downloaded:" | |
| ls -la current-* | |
| - name: Preserve current branch files | |
| run: | | |
| # Create a temp directory to store current branch files | |
| mkdir -p /tmp/current-branch-files | |
| # Move current branch files to temp directory | |
| mv current-* /tmp/current-branch-files/ 2>/dev/null || echo "No current-* files to preserve" | |
| echo "Preserved current branch files for later restoration" | |
| - name: Stop current branch server | |
| run: | | |
| if [ -f /tmp/current-server.pid ]; then | |
| echo "Stopping current branch server..." | |
| kill $(cat /tmp/current-server.pid) || true | |
| # Wait a bit for graceful shutdown | |
| sleep 5 | |
| # Force kill if still running | |
| kill -9 $(cat /tmp/current-server.pid) 2>/dev/null || true | |
| rm -f /tmp/current-server.pid | |
| fi | |
| - name: Flush Redis between server runs | |
| run: | | |
| # Clear all Redis caches to prevent stale data from the current branch | |
| # server contaminating the main branch server. Both servers share the | |
| # same Redis instance, and CoreEntityCacheService/WorkspaceCacheService | |
| # persist cached entities across process restarts. | |
| redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis" | |
| - name: Checkout main branch | |
| run: | | |
| git stash | |
| git checkout origin/main | |
| git reset --hard | |
| git clean -xfd -ff | |
| rm -rf node_modules packages/*/node_modules packages/*/dist dist .nx/cache | |
| - name: Install dependencies for main branch | |
| uses: ./.github/actions/yarn-install | |
| - name: Build main branch dependencies | |
| run: | | |
| npx nx reset | |
| npx nx build twenty-shared | |
| npx nx build twenty-emails | |
| - name: Build main branch server | |
| run: npx nx build twenty-server | |
| - name: Setup main branch database | |
| run: | | |
| npx nx reset:env twenty-server | |
| set_env_var() { | |
| local var_name="$1" | |
| local var_value="$2" | |
| local env_file="packages/twenty-server/.env" | |
| echo "" >> "$env_file" | |
| if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then | |
| sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file" | |
| else | |
| echo "${var_name}=${var_value}" >> "$env_file" | |
| fi | |
| } | |
| set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch" | |
| set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}" | |
| set_env_var "REDIS_URL" "redis://localhost:6379" | |
| set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty" | |
| set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword" | |
| npx nx run twenty-server:database:init:prod | |
| - name: Flush cache before seeding main branch | |
| run: npx nx command-no-deps twenty-server -- cache:flush | |
| - name: Seed main branch database with test data | |
| run: | | |
| npx nx command-no-deps twenty-server -- workspace:seed:dev | |
| - name: Start main branch server in background | |
| run: | | |
| echo "=== Main branch .env file contents ===" | |
| cat packages/twenty-server/.env | |
| echo "=== Starting main branch server ===" | |
| nohup npx nx run twenty-server:start:prod > /tmp/main-server.log 2>&1 & | |
| echo $! > /tmp/main-server.pid | |
| echo "Main server PID: $(cat /tmp/main-server.pid)" | |
| - name: Wait for main branch server to be ready | |
| run: | | |
| echo "Waiting for main branch server to start..." | |
| timeout=300 | |
| interval=5 | |
| elapsed=0 | |
| ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json) | |
| while [ $elapsed -lt $timeout ]; do | |
| GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}') | |
| if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \ | |
| curl -fsS "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then | |
| echo "Main branch server is ready!" | |
| break | |
| fi | |
| echo "Main branch server not ready yet, waiting ${interval}s..." | |
| sleep $interval | |
| elapsed=$((elapsed + interval)) | |
| done | |
| if [ $elapsed -ge $timeout ]; then | |
| echo "β Timed out waiting for main branch server to serve a valid schema." | |
| echo "Main server log:" | |
| cat /tmp/main-server.log || echo "No main server log found" | |
| exit 1 | |
| fi | |
| - name: Download GraphQL and REST responses from main branch | |
| run: | | |
| # Read admin token from shared test tokens file (single source of truth) | |
| ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json) | |
| # Load introspection query from file | |
| INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql) | |
| # Prepare the query payload | |
| QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g') | |
| echo "Downloading GraphQL schema from main server..." | |
| curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -d "{\"query\":\"${QUERY_PAYLOAD}\"}" \ | |
| -o main-schema-introspection.json \ | |
| -w "HTTP Status: %{http_code}\n" \ | |
| -s | |
| echo "Downloading GraphQL metadata schema from main server..." | |
| curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/metadata" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -d "{\"query\":\"${QUERY_PAYLOAD}\"}" \ | |
| -o main-metadata-schema-introspection.json \ | |
| -w "HTTP Status: %{http_code}\n" \ | |
| -s | |
| # Download main branch OpenAPI specs | |
| echo "Downloading OpenAPI specifications from main server..." | |
| curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -o main-rest-api.json \ | |
| -w "HTTP Status: %{http_code}\n" | |
| curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/metadata" \ | |
| -H "Authorization: Bearer ${ADMIN_TOKEN}" \ | |
| -o main-rest-metadata-api.json \ | |
| -w "HTTP Status: %{http_code}\n" | |
| # Verify the downloads | |
| echo "Main branch files downloaded:" | |
| ls -la main-* | |
| - name: Restore current branch files | |
| run: | | |
| # Move current branch files back to working directory | |
| mv /tmp/current-branch-files/* . 2>/dev/null || echo "No files to restore" | |
| # Verify all files are present | |
| echo "All API files restored:" | |
| ls -la current-* main-* 2>/dev/null || echo "Some files may be missing" | |
| # Clean up temp directory | |
| rm -rf /tmp/current-branch-files | |
| - name: Validate downloaded schema files | |
| id: validate-schemas | |
| run: | | |
| valid=true | |
| for file in main-schema-introspection.json current-schema-introspection.json \ | |
| main-metadata-schema-introspection.json current-metadata-schema-introspection.json; do | |
| if [ ! -f "$file" ]; then | |
| echo "::warning::Missing GraphQL schema file: $file" | |
| valid=false | |
| elif ! jq -e '.data.__schema' "$file" >/dev/null 2>&1; then | |
| echo "::warning::File $file is not a valid GraphQL introspection result. First 200 bytes: $(head -c 200 "$file")" | |
| valid=false | |
| fi | |
| done | |
| for file in main-rest-api.json current-rest-api.json \ | |
| main-rest-metadata-api.json current-rest-metadata-api.json; do | |
| if [ ! -f "$file" ]; then | |
| echo "::warning::Missing OpenAPI spec file: $file" | |
| valid=false | |
| elif ! jq -e '.openapi // .swagger' "$file" >/dev/null 2>&1; then | |
| echo "::warning::File $file is not a valid OpenAPI spec. First 200 bytes: $(head -c 200 "$file")" | |
| valid=false | |
| fi | |
| done | |
| echo "valid=$valid" >> $GITHUB_OUTPUT | |
| - name: Install OpenAPI Diff Tool | |
| run: | | |
| # Using the Java-based OpenAPITools/openapi-diff via Docker | |
| echo "Using OpenAPITools/openapi-diff via Docker" | |
| - name: Generate GraphQL Schema Diff Reports | |
| id: graphql-diff | |
| if: steps.validate-schemas.outputs.valid == 'true' | |
| run: | | |
| echo "=== INSTALLING GRAPHQL INSPECTOR CLI ===" | |
| npm install -g @graphql-inspector/cli | |
| echo "=== GENERATING GRAPHQL DIFF REPORTS ===" | |
| # Check if GraphQL schema has changes | |
| echo "Checking GraphQL schema for changes..." | |
| if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then | |
| echo "β No changes in GraphQL schema" | |
| else | |
| echo "β οΈ Changes detected in GraphQL schema, generating report..." | |
| echo "core_breaking=true" >> $GITHUB_OUTPUT | |
| echo "# GraphQL Schema Changes" > graphql-schema-diff.md | |
| echo "" >> graphql-schema-diff.md | |
| graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || { | |
| echo "β οΈ **Breaking changes or errors detected in GraphQL schema**" >> graphql-schema-diff.md | |
| echo "" >> graphql-schema-diff.md | |
| echo "\`\`\`" >> graphql-schema-diff.md | |
| graphql-inspector diff main-schema-introspection.json current-schema-introspection.json 2>&1 >> graphql-schema-diff.md || echo "Error generating diff" >> graphql-schema-diff.md | |
| echo "\`\`\`" >> graphql-schema-diff.md | |
| } | |
| fi | |
| # Check if GraphQL metadata schema has changes | |
| echo "Checking GraphQL metadata schema for changes..." | |
| if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then | |
| echo "β No changes in GraphQL metadata schema" | |
| else | |
| echo "β οΈ Changes detected in GraphQL metadata schema, generating report..." | |
| echo "metadata_breaking=true" >> $GITHUB_OUTPUT | |
| echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md | |
| echo "" >> graphql-metadata-diff.md | |
| graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || { | |
| echo "β οΈ **Breaking changes or errors detected in GraphQL metadata schema**" >> graphql-metadata-diff.md | |
| echo "" >> graphql-metadata-diff.md | |
| echo "\`\`\`" >> graphql-metadata-diff.md | |
| graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json 2>&1 >> graphql-metadata-diff.md || echo "Error generating diff" >> graphql-metadata-diff.md | |
| echo "\`\`\`" >> graphql-metadata-diff.md | |
| } | |
| fi | |
| # Show summary | |
| echo "Generated diff files:" | |
| ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)" | |
| - name: Check REST API Breaking Changes | |
| id: rest-diff | |
| if: steps.validate-schemas.outputs.valid == 'true' | |
| run: | | |
| echo "=== CHECKING REST API FOR BREAKING CHANGES ===" | |
| # Use the Java-based openapi-diff via Docker | |
| docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \ | |
| --json /specs/rest-api-diff.json \ | |
| /specs/main-rest-api.json /specs/current-rest-api.json || echo "OpenAPI diff completed with exit code $?" | |
| # Check if the output file was created and is valid JSON | |
| if [ -f "rest-api-diff.json" ] && jq empty rest-api-diff.json 2>/dev/null; then | |
| # Check for breaking changes using Java openapi-diff JSON structure | |
| incompatible=$(jq -r '.incompatible // false' rest-api-diff.json) | |
| different=$(jq -r '.different // false' rest-api-diff.json) | |
| # Count changes | |
| new_endpoints=$(jq -r '.newEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0") | |
| missing_endpoints=$(jq -r '.missingEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0") | |
| changed_operations=$(jq -r '.changedOperations | length' rest-api-diff.json 2>/dev/null || echo "0") | |
| if [ "$incompatible" = "true" ]; then | |
| echo "β Breaking changes detected in REST API" | |
| echo "breaking=true" >> $GITHUB_OUTPUT | |
| # Generate breaking changes report | |
| echo "# REST API Breaking Changes" > rest-api-diff.md | |
| echo "" >> rest-api-diff.md | |
| echo "β οΈ **Breaking changes detected that may affect existing API consumers**" >> rest-api-diff.md | |
| echo "" >> rest-api-diff.md | |
| # Parse and format the changes from Java openapi-diff | |
| jq -r ' | |
| if (.missingEndpoints | length) > 0 then | |
| "## π¨ Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" + | |
| (.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n")) | |
| else "" end, | |
| if (.changedOperations | length) > 0 then | |
| "\n## β οΈ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" + | |
| (.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n")) | |
| else "" end, | |
| if (.newEndpoints | length) > 0 then | |
| "\n## β New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" + | |
| (.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n")) | |
| else "" end | |
| ' rest-api-diff.json >> rest-api-diff.md | |
| elif [ "$different" = "true" ]; then | |
| echo "π Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed) - no PR comment will be posted" | |
| # Don't create markdown file for non-breaking changes to avoid PR comments | |
| else | |
| echo "β No changes detected in REST API" | |
| # Don't create diff file for no changes | |
| fi | |
| else | |
| echo "β οΈ OpenAPI diff tool could not process the files" | |
| echo "# REST API Analysis Error" > rest-api-diff.md | |
| echo "" >> rest-api-diff.md | |
| echo "β οΈ **Error occurred while analyzing REST API changes**" >> rest-api-diff.md | |
| echo "" >> rest-api-diff.md | |
| echo "## Error Output" >> rest-api-diff.md | |
| echo "\`\`\`" >> rest-api-diff.md | |
| docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-api.json /specs/current-rest-api.json 2>&1 >> rest-api-diff.md || echo "Could not capture error output" | |
| echo "\`\`\`" >> rest-api-diff.md | |
| # Don't fail the workflow for tool errors | |
| echo "::warning::REST API analysis tool error - continuing workflow" | |
| fi | |
| - name: Check REST Metadata API Breaking Changes | |
| id: rest-metadata-diff | |
| if: steps.validate-schemas.outputs.valid == 'true' | |
| run: | | |
| echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ===" | |
| # Use the Java-based openapi-diff for metadata API as well | |
| docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \ | |
| --json /specs/rest-metadata-api-diff.json \ | |
| /specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json || echo "OpenAPI diff completed with exit code $?" | |
| # Check if the output file was created and is valid JSON | |
| if [ -f "rest-metadata-api-diff.json" ] && jq empty rest-metadata-api-diff.json 2>/dev/null; then | |
| # Check for breaking changes using Java openapi-diff JSON structure | |
| incompatible=$(jq -r '.incompatible // false' rest-metadata-api-diff.json) | |
| different=$(jq -r '.different // false' rest-metadata-api-diff.json) | |
| # Count changes | |
| new_endpoints=$(jq -r '.newEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0") | |
| missing_endpoints=$(jq -r '.missingEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0") | |
| changed_operations=$(jq -r '.changedOperations | length' rest-metadata-api-diff.json 2>/dev/null || echo "0") | |
| if [ "$incompatible" = "true" ]; then | |
| echo "β Breaking changes detected in REST Metadata API" | |
| echo "breaking=true" >> $GITHUB_OUTPUT | |
| # Generate breaking changes report (only for breaking changes) | |
| echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md | |
| echo "" >> rest-metadata-api-diff.md | |
| echo "β οΈ **Breaking changes detected that may affect existing API consumers**" >> rest-metadata-api-diff.md | |
| echo "" >> rest-metadata-api-diff.md | |
| # Parse and format the changes from Java openapi-diff | |
| jq -r ' | |
| if (.missingEndpoints | length) > 0 then | |
| "## π¨ Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" + | |
| (.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n")) | |
| else "" end, | |
| if (.changedOperations | length) > 0 then | |
| "\n## β οΈ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" + | |
| (.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n")) | |
| else "" end, | |
| if (.newEndpoints | length) > 0 then | |
| "\n## β New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" + | |
| (.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n")) | |
| else "" end | |
| ' rest-metadata-api-diff.json >> rest-metadata-api-diff.md | |
| elif [ "$different" = "true" ]; then | |
| echo "π Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed) - no PR comment will be posted" | |
| # Don't create markdown file for non-breaking changes to avoid PR comments | |
| else | |
| echo "β No changes detected in REST Metadata API" | |
| fi | |
| else | |
| echo "β οΈ OpenAPI diff tool could not process the metadata API files" | |
| echo "# REST Metadata API Analysis Error" > rest-metadata-api-diff.md | |
| echo "" >> rest-metadata-api-diff.md | |
| echo "β οΈ **Error occurred while analyzing REST Metadata API changes**" >> rest-metadata-api-diff.md | |
| echo "" >> rest-metadata-api-diff.md | |
| echo "## Error Output" >> rest-metadata-api-diff.md | |
| echo "\`\`\`" >> rest-metadata-api-diff.md | |
| docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json 2>&1 >> rest-metadata-api-diff.md || echo "Could not capture error output" | |
| echo "\`\`\`" >> rest-metadata-api-diff.md | |
| # Don't fail the workflow for tool errors | |
| echo "::warning::REST Metadata API analysis tool error - continuing workflow" | |
| fi | |
| - name: Fail on breaking changes | |
| if: steps.validate-schemas.outputs.valid == 'true' | |
| run: | | |
| breaking=false | |
| if [ "${{ steps.graphql-diff.outputs.core_breaking }}" = "true" ]; then | |
| echo "β GraphQL core schema has breaking changes" | |
| breaking=true | |
| if [ -f graphql-schema-diff.md ]; then | |
| echo "" | |
| cat graphql-schema-diff.md | |
| echo "" | |
| fi | |
| fi | |
| if [ "${{ steps.graphql-diff.outputs.metadata_breaking }}" = "true" ]; then | |
| echo "β GraphQL metadata schema has breaking changes" | |
| breaking=true | |
| if [ -f graphql-metadata-diff.md ]; then | |
| echo "" | |
| cat graphql-metadata-diff.md | |
| echo "" | |
| fi | |
| fi | |
| if [ "${{ steps.rest-diff.outputs.breaking }}" = "true" ]; then | |
| echo "β REST core API has breaking changes" | |
| breaking=true | |
| if [ -f rest-api-diff.json ]; then | |
| echo "" | |
| jq -r ' | |
| (if (.missingEndpoints | length) > 0 then | |
| " Removed endpoints:\n" + | |
| (.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n")) | |
| else "" end), | |
| (if (.changedOperations | length) > 0 then | |
| " Changed operations:\n" + | |
| (.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n")) | |
| else "" end) | |
| ' rest-api-diff.json | sed '/^$/d' | |
| echo "" | |
| fi | |
| fi | |
| if [ "${{ steps.rest-metadata-diff.outputs.breaking }}" = "true" ]; then | |
| echo "β REST metadata API has breaking changes" | |
| breaking=true | |
| if [ -f rest-metadata-api-diff.json ]; then | |
| echo "" | |
| jq -r ' | |
| (if (.missingEndpoints | length) > 0 then | |
| " Removed endpoints:\n" + | |
| (.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n")) | |
| else "" end), | |
| (if (.changedOperations | length) > 0 then | |
| " Changed operations:\n" + | |
| (.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n")) | |
| else "" end) | |
| ' rest-metadata-api-diff.json | sed '/^$/d' | |
| echo "" | |
| fi | |
| fi | |
| if [ "$breaking" = "true" ]; then | |
| echo "" | |
| echo "This PR introduces breaking changes to the public API." | |
| echo "If intentional, deprecate the old endpoint and introduce a new one." | |
| echo "See the breaking changes report artifact and PR comment for details." | |
| exit 1 | |
| fi | |
| echo "β No breaking API changes detected" | |
| - name: Upload breaking changes report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: breaking-changes-report | |
| path: | | |
| *-diff.md | |
| *-diff.json | |
| if-no-files-found: ignore | |
| retention-days: 3 | |
| - name: Cleanup servers | |
| if: always() | |
| run: | | |
| if [ -f /tmp/current-server.pid ]; then | |
| kill $(cat /tmp/current-server.pid) || true | |
| fi | |
| if [ -f /tmp/main-server.pid ]; then | |
| kill $(cat /tmp/main-server.pid) || true | |
| fi | |