mirror of
https://github.com/twentyhq/twenty.git
synced 2026-02-01 11:32:27 -05:00
## Summary This PR fixes translation QA issues and adds automation to prevent future issues. ### Translation Fixes - Fixed **escaped Unicode sequences** in translations (e.g., `\u62db\u5f85` → `招待`) - Removed **corrupted control characters** from .po files (null bytes, invalid characters) - Fixed **missing/incorrect placeholders** in various languages - Deleted **35 problematic translations** via Crowdin API that had variable mismatches ### New Scripts (in `packages/twenty-utils/`) - `fix-crowdin-translations.ts` - Auto-fixes encoding issues and syncs to Crowdin - `fix-qa-issues.ts` - Fixes specific QA issues via Crowdin API - `translation-qa-report.ts` - Generates weekly QA report from Crowdin API ### New Workflow - `i18n-qa-report.yaml` - Weekly workflow that creates a PR with translation QA issues for review ### Other Changes - Moved GitHub Actions from `.github/workflows/actions/` to `.github/actions/` - Fixed `date-utils.ts` to avoid nested `t` macros in plural expressions (root cause of confusing placeholders) ### QA Status After Fixes | Category | Count | Status | |----------|-------|--------| | variables | 0 ✅ | Fixed | | tags | 1 | Minor | | empty | 0 ✅ | Fixed | | spaces | 127 | Low priority | | numbers | 246 | Locale-specific | | special_symbols | 268 | Locale-specific |
787 lines
35 KiB
YAML
787 lines
35 KiB
YAML
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
|
||
pull-requests: write
|
||
checks: write
|
||
|
||
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: twentycrm/twenty-postgres-spilo
|
||
env:
|
||
PGUSER_SUPERUSER: postgres
|
||
PGPASSWORD_SUPERUSER: postgres
|
||
ALLOW_NOSSL: 'true'
|
||
SPILO_PROVIDER: 'local'
|
||
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@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Try to merge main into current branch
|
||
id: merge_attempt
|
||
run: |
|
||
echo "Attempting to merge main into current branch..."
|
||
|
||
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
|
||
git merge --abort
|
||
|
||
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
|
||
npx nx run twenty-server:database:migrate:prod
|
||
|
||
- 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=300
|
||
interval=5
|
||
elapsed=0
|
||
|
||
while [ $elapsed -lt $timeout ]; do
|
||
if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
|
||
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /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 "Timeout waiting for current branch server to start"
|
||
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: 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
|
||
npx nx run twenty-server:database:migrate:prod
|
||
|
||
- 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
|
||
|
||
while [ $elapsed -lt $timeout ]; do
|
||
if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
|
||
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /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 "Timeout waiting for main branch server to start"
|
||
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: 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
|
||
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"
|
||
# Don't create a diff file for no changes
|
||
else
|
||
echo "⚠️ Changes detected in GraphQL schema, generating report..."
|
||
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"
|
||
# Don't create a diff file for no changes
|
||
else
|
||
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
|
||
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
|
||
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"
|
||
|
||
# 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
|
||
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"
|
||
|
||
# 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: Comment API Changes on PR
|
||
if: always()
|
||
uses: actions/github-script@v7
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
let hasChanges = false;
|
||
let comment = '';
|
||
|
||
try {
|
||
if (fs.existsSync('graphql-schema-diff.md')) {
|
||
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
|
||
if (graphqlDiff.trim()) {
|
||
if (!hasChanges) {
|
||
comment = '## 📊 API Changes Report\n\n';
|
||
hasChanges = true;
|
||
}
|
||
comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n';
|
||
}
|
||
}
|
||
|
||
if (fs.existsSync('graphql-metadata-diff.md')) {
|
||
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
|
||
if (graphqlMetadataDiff.trim()) {
|
||
if (!hasChanges) {
|
||
comment = '## 📊 API Changes Report\n\n';
|
||
hasChanges = true;
|
||
}
|
||
comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n';
|
||
}
|
||
}
|
||
|
||
if (fs.existsSync('rest-api-diff.md')) {
|
||
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
|
||
if (restDiff.trim()) {
|
||
if (!hasChanges) {
|
||
comment = '## 📊 API Changes Report\n\n';
|
||
hasChanges = true;
|
||
}
|
||
comment += restDiff + '\n\n';
|
||
}
|
||
}
|
||
|
||
if (fs.existsSync('rest-metadata-api-diff.md')) {
|
||
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
|
||
if (metadataDiff.trim()) {
|
||
if (!hasChanges) {
|
||
comment = '## 📊 API Changes Report\n\n';
|
||
hasChanges = true;
|
||
}
|
||
comment += metadataDiff + '\n\n';
|
||
}
|
||
}
|
||
|
||
// Only post comment if there are changes
|
||
if (hasChanges) {
|
||
// Add branch state information only if there were conflicts
|
||
const branchState = process.env.BRANCH_STATE || 'unknown';
|
||
let branchStateNote = '';
|
||
|
||
if (branchState === 'conflicts') {
|
||
branchStateNote = '\n\n⚠️ **Note**: Could not merge with `main` due to conflicts. This comparison shows changes between the current branch and `main` as separate states.\n';
|
||
}
|
||
// Check if there are any breaking changes detected
|
||
let hasBreakingChanges = false;
|
||
let breakingChangeNote = '';
|
||
|
||
// Check for breaking changes in any of the diff files
|
||
if (fs.existsSync('rest-api-diff.md')) {
|
||
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
|
||
if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') ||
|
||
restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) {
|
||
hasBreakingChanges = true;
|
||
}
|
||
}
|
||
|
||
if (fs.existsSync('rest-metadata-api-diff.md')) {
|
||
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
|
||
if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') ||
|
||
metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) {
|
||
hasBreakingChanges = true;
|
||
}
|
||
}
|
||
|
||
// Also check GraphQL changes for breaking changes indicators
|
||
if (fs.existsSync('graphql-schema-diff.md')) {
|
||
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
|
||
if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) {
|
||
hasBreakingChanges = true;
|
||
}
|
||
}
|
||
|
||
if (fs.existsSync('graphql-metadata-diff.md')) {
|
||
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
|
||
if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) {
|
||
hasBreakingChanges = true;
|
||
}
|
||
}
|
||
|
||
// Check PR title for "breaking"
|
||
const prTitle = ${{ toJSON(github.event.pull_request.title) }};
|
||
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
|
||
|
||
if (hasBreakingChanges) {
|
||
if (titleContainsBreaking) {
|
||
breakingChangeNote = '\n\n## ✅ Breaking Change Protocol\n\n' +
|
||
'**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' +
|
||
'📝 **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' +
|
||
'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
|
||
} else {
|
||
breakingChangeNote = '\n\n## ⚠️ Breaking Change Protocol\n\n' +
|
||
'**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' +
|
||
'🔄 **Options**:\n' +
|
||
'1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' +
|
||
'2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' +
|
||
'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
|
||
}
|
||
}
|
||
|
||
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
|
||
const commentBody = COMMENT_MARKER + '\n' + comment + branchStateNote + '\n⚠️ **Please review these API changes carefully before merging.**' + breakingChangeNote;
|
||
|
||
// Get all comments to find existing API changes comment
|
||
const {data: comments} = await github.rest.issues.listComments({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: context.issue.number,
|
||
});
|
||
|
||
// Find our existing comment
|
||
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
|
||
|
||
if (botComment) {
|
||
// Update existing comment
|
||
await github.rest.issues.updateComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
comment_id: botComment.id,
|
||
body: commentBody
|
||
});
|
||
console.log('Updated existing API changes comment');
|
||
} else {
|
||
// Create new comment
|
||
await github.rest.issues.createComment({
|
||
issue_number: context.issue.number,
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
body: commentBody
|
||
});
|
||
console.log('Created new API changes comment');
|
||
}
|
||
} else {
|
||
console.log('No API changes detected - skipping PR comment');
|
||
|
||
// Check if there's an existing comment to remove
|
||
const {data: comments} = await github.rest.issues.listComments({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: context.issue.number,
|
||
});
|
||
|
||
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
|
||
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
|
||
|
||
if (botComment) {
|
||
await github.rest.issues.deleteComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
comment_id: botComment.id,
|
||
});
|
||
console.log('Deleted existing API changes comment (no changes detected)');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log('Could not post comment:', error);
|
||
}
|
||
|
||
- 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
|
||
|
||
- name: Upload API specifications and diffs
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: api-specifications-and-diffs
|
||
path: |
|
||
/tmp/main-server.log
|
||
/tmp/current-server.log
|
||
*-api.json
|
||
*-schema-introspection.json
|
||
*-diff.md
|
||
*-diff.json
|
||
|