diff --git a/.github/workflows/trainer-rank-gpu.yml b/.github/workflows/trainer-rank-gpu.yml index bc1faebfd..0d208ad19 100644 --- a/.github/workflows/trainer-rank-gpu.yml +++ b/.github/workflows/trainer-rank-gpu.yml @@ -15,15 +15,70 @@ concurrency: jobs: classify: runs-on: ubuntu-latest + permissions: + actions: read + contents: read outputs: required: ${{ steps.changes.outputs.required }} head_sha: ${{ steps.changes.outputs.head_sha }} + reused: ${{ steps.reuse.outputs.reused }} + reused_run: ${{ steps.reuse.outputs.run_id }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 + - id: reuse + name: Find exact prior GPU validation + if: github.event_name == 'pull_request' && github.event.action == 'ready_for_review' + continue-on-error: true + timeout-minutes: 2 + uses: actions/github-script@v7 + with: + script: | + const pull = context.payload.pull_request; + const runs = await github.rest.actions.listWorkflowRuns({ + ...context.repo, + workflow_id: 'trainer-rank-gpu.yml', + event: 'pull_request', + head_sha: pull.head.sha, + status: 'success', + per_page: 100, + }); + const candidates = runs.data.workflow_runs.filter(run => + run.id !== context.runId && + run.head_sha === pull.head.sha && + run.path === '.github/workflows/trainer-rank-gpu.yml' && + (run.pull_requests ?? []).some(previous => previous.number === pull.number) + ).slice(0, 10); + for (const run of candidates) { + const jobs = await github.rest.actions.listJobsForWorkflowRun({ + ...context.repo, + run_id: run.id, + filter: 'latest', + per_page: 100, + }); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + ...context.repo, + run_id: run.id, + per_page: 100, + }); + const passed = jobs.data.jobs.some(job => + job.name === 'Run on 2x H200' && job.conclusion === 'success' + ); + const base = artifacts.data.artifacts.some(artifact => + !artifact.expired && artifact.name === + `trainer-rank-result-${run.id}-${run.run_attempt}-${pull.base.sha}` + ); + if (passed && base) { + core.setOutput('reused', 'true'); + core.setOutput('run_id', String(run.id)); + core.info(`Reusing successful workflow run ${run.id}.`); + return; + } + } + - id: changes name: Classify changed files env: @@ -65,7 +120,7 @@ jobs: validate: name: Run on 2x H200 needs: classify - if: needs.classify.outputs.required == 'true' + if: needs.classify.outputs.required == 'true' && needs.classify.outputs.reused != 'true' runs-on: ubuntu-latest timeout-minutes: 45 environment: trainer-rank-gpu-validation @@ -154,7 +209,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: trainer-rank-result-${{ github.run_id }}-${{ github.run_attempt }} + name: trainer-rank-result-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.event.pull_request.base.sha || 'manual' }} path: ${{ runner.temp }}/trainer-rank-result if-no-files-found: warn @@ -167,17 +222,26 @@ jobs: - env: CLASSIFY_RESULT: ${{ needs.classify.result }} REQUIRED: ${{ needs.classify.outputs.required }} + REUSED: ${{ needs.classify.outputs.reused }} + REUSED_RUN: ${{ needs.classify.outputs.reused_run }} RESULT: ${{ needs.validate.result }} run: | + set -euo pipefail if [ "${CLASSIFY_RESULT}" != "success" ]; then echo "TrainerRank GPU classification failed." >&2 exit 1 fi - if [ "${REQUIRED}" = "true" ] && [ "${RESULT}" != "success" ]; then + if [ "${REQUIRED}" = "true" ] \ + && [ "${REUSED}" != "true" ] \ + && [ "${RESULT}" != "success" ]; then echo "TrainerRank GPU validation required but result was ${RESULT}." >&2 exit 1 fi - if [ "${REQUIRED}" = "true" ]; then + if [ "${REUSED}" = "true" ]; then + test -n "${REUSED_RUN}" + echo "TrainerRank GPU validation reused from exact run ${REUSED_RUN}." \ + | tee -a "${GITHUB_STEP_SUMMARY}" + elif [ "${REQUIRED}" = "true" ]; then echo "TrainerRank GPU validation passed." else echo "TrainerRank GPU validation was not required."