Stage InferenceX PR #1
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: Stage InferenceX Results | ||
| run-name: Stage InferenceX PR #${{ github.event.client_payload.pr-number || inputs.pr-number }} run ${{ github.event.client_payload.run-id || inputs.run-id }} | ||
| on: | ||
| repository_dispatch: | ||
| types: [stage-results] | ||
| workflow_dispatch: | ||
| inputs: | ||
| run-id: | ||
| description: InferenceX Actions run ID to stage | ||
| required: true | ||
| type: string | ||
| run-attempt: | ||
| description: InferenceX Actions run attempt to stage | ||
| required: false | ||
| default: '1' | ||
| type: string | ||
| run-date: | ||
| description: UTC date of the InferenceX run (YYYY-MM-DD) | ||
| required: true | ||
| type: string | ||
| pr-number: | ||
| description: InferenceX pull request number | ||
| required: true | ||
| type: string | ||
| requested-by: | ||
| description: GitHub login that requested staging | ||
| required: true | ||
| type: string | ||
| permissions: {} | ||
| concurrency: | ||
| group: inferencex-shared-staging | ||
| cancel-in-progress: false | ||
| jobs: | ||
| validate: | ||
| name: Validate staging request | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| run-id: ${{ steps.request.outputs.run-id }} | ||
| run-attempt: ${{ steps.request.outputs.run-attempt }} | ||
| run-date: ${{ steps.request.outputs.run-date }} | ||
| pr-number: ${{ steps.request.outputs.pr-number }} | ||
| requested-by: ${{ steps.request.outputs.requested-by }} | ||
| steps: | ||
| - name: Validate request payload | ||
| id: request | ||
| env: | ||
| RUN_ID: ${{ github.event.client_payload.run-id || inputs.run-id }} | ||
| RUN_ATTEMPT: ${{ github.event.client_payload.run-attempt || inputs.run-attempt || '1' }} | ||
| RUN_DATE: ${{ github.event.client_payload.run-date || inputs.run-date }} | ||
| PR_NUMBER: ${{ github.event.client_payload.pr-number || inputs.pr-number }} | ||
| REQUESTED_BY: ${{ github.event.client_payload.requested-by || inputs.requested-by }} | ||
| SOURCE_REPOSITORY: ${{ github.event.client_payload.source-repository || 'SemiAnalysisAI/InferenceX' }} | ||
| run: | | ||
| if [[ ! "$RUN_ID" =~ ^[0-9]+$ ]]; then | ||
| echo "::error::run-id must be numeric" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$RUN_ATTEMPT" =~ ^[0-9]+$ ]]; then | ||
| echo "::error::run-attempt must be numeric" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$RUN_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then | ||
| echo "::error::run-date must use YYYY-MM-DD" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then | ||
| echo "::error::pr-number must be numeric" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$REQUESTED_BY" =~ ^[A-Za-z0-9-]+$ ]]; then | ||
| echo "::error::requested-by is not a valid GitHub login" | ||
| exit 1 | ||
| fi | ||
| if [ "$SOURCE_REPOSITORY" != "SemiAnalysisAI/InferenceX" ]; then | ||
| echo "::error::Unsupported source repository: $SOURCE_REPOSITORY" | ||
| exit 1 | ||
| fi | ||
| { | ||
| echo "run-id=$RUN_ID" | ||
| echo "run-attempt=$RUN_ATTEMPT" | ||
| echo "run-date=$RUN_DATE" | ||
| echo "pr-number=$PR_NUMBER" | ||
| echo "requested-by=$REQUESTED_BY" | ||
| } >> "$GITHUB_OUTPUT" | ||
| sync-staging-branch: | ||
| name: Fast-forward Git staging branch | ||
| needs: validate | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write # Point the shared staging Git branch at master | ||
| steps: | ||
| - name: Point staging at master | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| script: | | ||
| const source = await github.rest.git.getRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: 'heads/master', | ||
| }); | ||
| const sha = source.data.object.sha; | ||
| try { | ||
| const target = await github.rest.git.getRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: 'heads/staging', | ||
| }); | ||
| if (target.data.object.sha !== sha) { | ||
| await github.rest.git.updateRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: 'heads/staging', | ||
| sha, | ||
| force: true, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| if (error.status !== 404) throw error; | ||
| await github.rest.git.createRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: 'refs/heads/staging', | ||
| sha, | ||
| }); | ||
| } | ||
| core.notice(`staging now points to ${sha}`); | ||
| refresh-staging-database: | ||
| name: Refresh Neon staging branch | ||
| needs: [validate, sync-staging-branch] | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| NEON_API_KEY: ${{ secrets.NEON_API_KEY }} | ||
| NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }} | ||
| steps: | ||
| - name: Restore staging from production head | ||
| run: | | ||
| if [ -z "$NEON_API_KEY" ] || [ -z "$NEON_PROJECT_ID" ]; then | ||
| echo "::error::NEON_API_KEY and NEON_PROJECT_ID are required" | ||
| exit 1 | ||
| fi | ||
| api="https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID" | ||
| branches=$(curl --retry 3 --retry-all-errors -sSf \ | ||
| -H "Authorization: Bearer $NEON_API_KEY" \ | ||
| "$api/branches?limit=100") | ||
| source_branch_id=$(jq -r '.branches[] | select(.default == true) | .id' <<<"$branches" | head -n 1) | ||
| staging_branch_id=$(jq -r '.branches[] | select(.name == "staging") | .id' <<<"$branches" | head -n 1) | ||
| if [ -z "$source_branch_id" ] || [ "$source_branch_id" = "null" ]; then | ||
| echo "::error::Neon default production branch was not found" | ||
| exit 1 | ||
| fi | ||
| if [ -z "$staging_branch_id" ] || [ "$staging_branch_id" = "null" ]; then | ||
| echo "::error::Neon branch named staging was not found" | ||
| exit 1 | ||
| fi | ||
| if [ "$source_branch_id" = "$staging_branch_id" ]; then | ||
| echo "::error::Refusing to restore the production branch" | ||
| exit 1 | ||
| fi | ||
| payload=$(jq -cn --arg source_branch_id "$source_branch_id" '{source_branch_id: $source_branch_id}') | ||
| # This destructive refresh removes the previous unofficial staging ingest. | ||
| # Every request therefore starts with production data and replaces the shared staged run. | ||
| curl --retry 3 --retry-all-errors -sSf -X POST \ | ||
| -H "Authorization: Bearer $NEON_API_KEY" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "$payload" \ | ||
| "$api/branches/$staging_branch_id/restore" >/dev/null | ||
| for attempt in $(seq 1 60); do | ||
| branch=$(curl --retry 3 --retry-all-errors -sSf \ | ||
| -H "Authorization: Bearer $NEON_API_KEY" \ | ||
| "$api/branches/$staging_branch_id") | ||
| state=$(jq -r '.branch.current_state' <<<"$branch") | ||
| pending=$(jq -r '.branch.pending_state // empty' <<<"$branch") | ||
| if [ "$state" = "ready" ] && [ -z "$pending" ]; then | ||
| echo "Neon staging branch is ready" | ||
| exit 0 | ||
| fi | ||
| echo "Waiting for Neon staging branch (state=$state, pending=${pending:-none}, attempt=$attempt/60)" | ||
| sleep 10 | ||
| done | ||
| echo "::error::Timed out waiting for Neon staging branch restore" | ||
| exit 1 | ||
| ingest: | ||
|
Check failure on line 196 in .github/workflows/stage-results.yml
|
||
| name: Ingest selected run | ||
| needs: [validate, refresh-staging-database] | ||
| uses: ./.github/workflows/ingest-agentic-results.yml | ||
| with: | ||
| run-id: ${{ needs.validate.outputs.run-id }} | ||
| run-attempt: ${{ needs.validate.outputs.run-attempt }} | ||
| database-target: staging | ||
| secrets: | ||
| DATABASE_STAGING_WRITE_URL: ${{ secrets.DATABASE_STAGING_WRITE_URL }} | ||
| VERCEL_STAGING_INVALIDATE_SECRET: ${{ secrets.VERCEL_STAGING_INVALIDATE_SECRET }} | ||
| VERCEL_STAGING_BYPASS_SECRET: ${{ secrets.VERCEL_STAGING_BYPASS_SECRET }} | ||
| INFX_MAIN_PAT: ${{ secrets.INFX_MAIN_PAT }} | ||
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | ||
| report: | ||
| name: Report staging result | ||
| if: ${{ always() && needs.validate.result == 'success' }} | ||
| needs: [validate, sync-staging-branch, refresh-staging-database, ingest] | ||
| runs-on: ubuntu-latest | ||
| permissions: {} | ||
| steps: | ||
| - name: Comment on source pull request | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| RUN_ID: ${{ needs.validate.outputs.run-id }} | ||
| RUN_DATE: ${{ needs.validate.outputs.run-date }} | ||
| PR_NUMBER: ${{ needs.validate.outputs.pr-number }} | ||
| REQUESTED_BY: ${{ needs.validate.outputs.requested-by }} | ||
| STAGING_SITE_URL: ${{ vars.STAGING_SITE_URL || 'https://inferencemax-app-git-staging-semianalysisai.vercel.app' }} | ||
| STAGE_SUCCEEDED: ${{ needs['sync-staging-branch'].result == 'success' && needs['refresh-staging-database'].result == 'success' && needs.ingest.result == 'success' }} | ||
| with: | ||
| github-token: ${{ secrets.INFX_MAIN_PAT }} | ||
| script: | | ||
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | ||
| const site = process.env.STAGING_SITE_URL.replace(/\/$/u, ''); | ||
| const entry = encodeURIComponent(`${process.env.RUN_DATE}~r${process.env.RUN_ID}`); | ||
| const chartUrl = `${site}/inference?i_dates=${entry}`; | ||
| const success = process.env.STAGE_SUCCEEDED === 'true'; | ||
| const body = success | ||
| ? [ | ||
| `@${process.env.REQUESTED_BY} staged run ${process.env.RUN_ID}: ${chartUrl}`, | ||
| '', | ||
| `This shared staging slot remains available until the next \`/stage-results\` request. [Staging workflow](${runUrl})`, | ||
| '', | ||
| `@${process.env.REQUESTED_BY} 已将运行 ${process.env.RUN_ID} 发布到预发布环境:${chartUrl}`, | ||
| '', | ||
| `该共享预发布环境会保留到下一次 \`/stage-results\` 请求。[预发布工作流](${runUrl})`, | ||
| ].join('\n') | ||
| : [ | ||
| `@${process.env.REQUESTED_BY} staging run ${process.env.RUN_ID} failed. [Inspect the staging workflow](${runUrl}).`, | ||
| '', | ||
| `@${process.env.REQUESTED_BY} 预发布运行 ${process.env.RUN_ID} 失败。[查看预发布工作流](${runUrl})。`, | ||
| ].join('\n'); | ||
| await github.rest.issues.createComment({ | ||
| owner: 'SemiAnalysisAI', | ||
| repo: 'InferenceX', | ||
| issue_number: Number(process.env.PR_NUMBER), | ||
| body, | ||
| }); | ||