fix(kody-rules): trace the sharded judge in Langfuse #1332
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
| name: "Preview: Deploy (Web + Backend)" | |
| # ============================================================================= | |
| # Preview deploy for PRs. | |
| # - Railway handles backend deploys automatically (native PR environments) | |
| # - This workflow only handles Vercel (web) and PR comments | |
| # - When backend changes: Vercel points to Railway PR environment | |
| # - When only web changes: Vercel points to QA API | |
| # | |
| # Required secrets: | |
| # VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID | |
| # RAILWAY_TOKEN, RAILWAY_PROJECT_ID, RAILWAY_API_SERVICE_ID | |
| # ============================================================================= | |
| permissions: | |
| contents: read | |
| on: | |
| pull_request: | |
| types: [ opened, synchronize, reopened ] | |
| concurrency: | |
| group: preview-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ------------------------------------------------------------------ | |
| # 1. Detect what changed | |
| # ------------------------------------------------------------------ | |
| detect-changes: | |
| name: Detect changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| web: ${{ steps.changes.outputs.web }} | |
| backend: ${{ steps.changes.outputs.backend }} | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - uses: dorny/paths-filter@v4.0.1 | |
| id: changes | |
| with: | |
| filters: | | |
| web: | |
| - 'apps/web/**' | |
| backend: | |
| - 'apps/api/**' | |
| - 'apps/worker/**' | |
| - 'apps/webhooks/**' | |
| - 'libs/**' | |
| - 'packages/**' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'docker/Dockerfile' | |
| - 'docker-bake.hcl' | |
| # ------------------------------------------------------------------ | |
| # 2. Resolve Railway API URL (if backend changed) | |
| # | |
| # Railway exposes PR environments at a deterministic public host: | |
| # <service-slug>-<env-slug>.up.railway.app. We just construct it | |
| # instead of querying the Railway CLI — the previous approach | |
| # relied on `railway domain` which silently returned empty in our | |
| # setup, looped 30x, and wedged the workflow. | |
| # | |
| # Then we poll the URL until the API responds (Railway PR envs | |
| # take a couple of minutes to boot from cold). | |
| # ------------------------------------------------------------------ | |
| resolve-backend-url: | |
| name: Resolve backend URL | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| api_url: ${{ steps.url.outputs.api_url }} | |
| steps: | |
| - name: Construct Railway URL | |
| id: url | |
| run: | | |
| PR_NUM="${{ github.event.pull_request.number }}" | |
| API_URL="https://kodus-ai-kodus-ai-pr-${PR_NUM}.up.railway.app" | |
| echo "api_url=$API_URL" >> $GITHUB_OUTPUT | |
| echo "Backend URL: $API_URL" | |
| - name: Wait for Railway deployment | |
| run: | | |
| API_URL="${{ steps.url.outputs.api_url }}" | |
| echo "Waiting for $API_URL to respond..." | |
| for i in $(seq 1 30); do | |
| code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$API_URL/") || code="000" | |
| # Any non-5xx (incl. 404) means the API is up enough to | |
| # route. 502/503/000 mean Railway is still cold-starting | |
| # or networking isn't ready yet. | |
| if [ "$code" != "000" ] && [ "$code" -lt 500 ]; then | |
| echo "API is up (HTTP $code on attempt $i)" | |
| exit 0 | |
| fi | |
| echo "Attempt $i/30 - HTTP $code, retrying in 20s..." | |
| sleep 20 | |
| done | |
| echo "::error::API at $API_URL did not become reachable after 10 minutes" | |
| exit 1 | |
| # ------------------------------------------------------------------ | |
| # 3. Web preview on Vercel | |
| # ------------------------------------------------------------------ | |
| deploy-web: | |
| name: Deploy web preview | |
| needs: [ detect-changes, resolve-backend-url ] | |
| if: | | |
| always() && | |
| (needs.detect-changes.outputs.web == 'true' || needs.detect-changes.outputs.backend == 'true') && | |
| !cancelled() | |
| runs-on: ubuntu-latest | |
| environment: Preview | |
| outputs: | |
| preview_url: ${{ steps.deploy.outputs.preview_url }} | |
| env: | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Install Vercel CLI | |
| run: npm install -g vercel | |
| - name: Determine API URL | |
| id: api-url | |
| run: | | |
| BACKEND_CHANGED="${{ needs.detect-changes.outputs.backend }}" | |
| RAILWAY_URL="${{ needs.resolve-backend-url.outputs.api_url }}" | |
| if [ "$BACKEND_CHANGED" = "true" ]; then | |
| if [ -z "$RAILWAY_URL" ]; then | |
| echo "::error::Backend changed, but Railway preview URL could not be resolved." | |
| exit 1 | |
| fi | |
| API_HOST="$RAILWAY_URL" | |
| API_HOST="${API_HOST#https://}" | |
| API_HOST="${API_HOST#http://}" | |
| API_HOST="${API_HOST%/}" | |
| echo "url=$API_HOST" >> $GITHUB_OUTPUT | |
| echo "Using Railway backend: https://$API_HOST" | |
| else | |
| API_HOST="${{ vars.QA_API_URL }}" | |
| API_HOST="${API_HOST#https://}" | |
| API_HOST="${API_HOST#http://}" | |
| API_HOST="${API_HOST%/}" | |
| echo "url=$API_HOST" >> $GITHUB_OUTPUT | |
| echo "Using QA backend: https://$API_HOST" | |
| fi | |
| # Build remotely on Vercel and pass WEB_HOSTNAME_API as a | |
| # per-deployment env var. Doing the build locally with | |
| # `vercel build` was hitting `spawn sh ENOENT` from the | |
| # CLI's installCommand spawn in our monorepo layout. | |
| # | |
| # Run from the repo root (no working-directory) — the Vercel | |
| # project already has Root Directory configured as apps/web, | |
| # so cd-ing into it would make the CLI resolve apps/web/apps/web. | |
| - name: Deploy preview | |
| id: deploy | |
| run: | | |
| URL=$(vercel deploy \ | |
| --token=${{ secrets.VERCEL_TOKEN }} \ | |
| --env "WEB_HOSTNAME_API=${{ steps.api-url.outputs.url }}" \ | |
| --build-env "WEB_HOSTNAME_API=${{ steps.api-url.outputs.url }}" \ | |
| --yes) | |
| echo "preview_url=$URL" >> $GITHUB_OUTPUT | |
| echo "Deployed: $URL" | |
| # ------------------------------------------------------------------ | |
| # 4. Comment on PR with preview links | |
| # ------------------------------------------------------------------ | |
| comment: | |
| name: Comment preview URLs | |
| needs: [ detect-changes, resolve-backend-url, deploy-web ] | |
| if: always() && !cancelled() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Build comment body | |
| id: body | |
| run: | | |
| BODY="## Preview Environment\n\n" | |
| WEB_URL="${{ needs.deploy-web.outputs.preview_url }}" | |
| API_URL="${{ needs.resolve-backend-url.outputs.api_url }}" | |
| if [ -n "$WEB_URL" ]; then | |
| BODY+="**Web:** $WEB_URL\n" | |
| fi | |
| if [ -n "$API_URL" ]; then | |
| BODY+="**API:** $API_URL\n" | |
| elif [ "${{ needs.detect-changes.outputs.backend }}" == "true" ]; then | |
| BODY+="**API:** Railway deploying... (check Railway dashboard)\n" | |
| fi | |
| if [ -z "$WEB_URL" ] && [ -z "$API_URL" ] && [ "${{ needs.detect-changes.outputs.backend }}" != "true" ]; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| BODY+="\n---\n_Updated at $(date -u '+%Y-%m-%d %H:%M UTC')_" | |
| # Use delimiter for multiline output | |
| echo "comment<<EOF" >> $GITHUB_OUTPUT | |
| echo -e "$BODY" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create or update PR comment | |
| if: steps.body.outputs.skip != 'true' | |
| uses: actions/github-script@v9.0.0 | |
| with: | |
| script: | | |
| const body = `${{ steps.body.outputs.comment }}`; | |
| const marker = '## Preview Environment'; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } |