diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 208f635d8..9a8c4b70d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,5 +1,5 @@ name: End-to-End Tests -run-name: e2e Test - ${{ inputs.test-name || github.event.inputs.generate-cli-command }} +run-name: e2e Test - ${{ inputs.test-name || inputs.generate-cli-command || github.event.inputs.generate-cli-command }} on: workflow_dispatch: @@ -12,6 +12,24 @@ on: description: "Name for this test run" required: false type: string + ref: + description: "Ref (branch/sha) to checkout for generating configs" + required: false + type: string + workflow_call: + inputs: + generate-cli-command: + description: "Command passed to generate matrix script" + required: true + type: string + test-name: + description: "Name for this test run" + required: false + type: string + ref: + description: "Ref (branch/sha) to checkout for generating configs" + required: false + type: string jobs: get-jobs: @@ -19,14 +37,21 @@ jobs: outputs: search-space-config: ${{ steps.get-jobs.outputs.search-space-config }} steps: - - name: Checkout code + - name: Checkout code (ref) + if: ${{ inputs.ref && inputs.ref != '' }} + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ inputs.ref }} + + - name: Checkout code (default) + if: ${{ !inputs.ref || inputs.ref == '' }} uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - id: get-jobs run: | pip install pydantic CONFIG_JSON=$(python3 ${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py \ - ${{ inputs.generate-cli-command }} \ + ${{ inputs.generate-cli-command || github.event.inputs.generate-cli-command }} \ --runner-config .github/configs/runners.yaml \ --config-files .github/configs/nvidia-master.yaml .github/configs/amd-master.yaml) echo "search-space-config=$CONFIG_JSON" >> $GITHUB_OUTPUT diff --git a/.github/workflows/pr-comment-sweep.yml b/.github/workflows/pr-comment-sweep.yml new file mode 100644 index 000000000..a66b4dc7a --- /dev/null +++ b/.github/workflows/pr-comment-sweep.yml @@ -0,0 +1,146 @@ +name: Slash Command Sweep +run-name: "Validate PR #${{ github.event.issue.number }}" + +on: + issue_comment: + types: [created] + +concurrency: + group: "PR#${{ github.event.issue.number || github.ref_name }}" + cancel-in-progress: true + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + get-jobs: + # Only run for PR comments that start with /sweep + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/sweep') }} + runs-on: ubuntu-latest + outputs: + pr-number: ${{ steps.parse.outputs.pr-number }} + generator-args: ${{ steps.parse.outputs.generator-args }} + author-can-bypass: ${{ steps.auth.outputs.can-bypass }} + # Immutable ref (commit SHA) to prevent TOCTOU on refs/pull//head + ref: ${{ steps.ref_comment.outputs.ref }} + steps: + - name: Parse PR comment (/sweep ...) + id: parse + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/sweep') }} + shell: bash + env: + BODY: ${{ github.event.comment.body }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + set -euo pipefail + # Require /sweep at the start of the line + cmd_line=$(printf "%s" "$BODY" | awk '/^\/sweep/{print; exit}') + if [[ -z "$cmd_line" ]]; then + echo "No /sweep command found at comment start" >&2 + exit 1 + fi + if [[ "$cmd_line" == "/sweep" ]]; then + cmd_args="" + else + cmd_args=${cmd_line#/sweep} + fi + cmd_args=$(echo "$cmd_args" | xargs || true) + + echo "generator-args=$cmd_args" >> "$GITHUB_OUTPUT" + echo "pr-number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + - name: Check author permissions + id: auth + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const username = context.payload.comment?.user?.login; + let permission = 'none'; + try { + const res = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); + permission = res.data?.permission || 'none'; + } catch (e) { + permission = 'none'; + } + const canBypass = ['admin','maintain','write'].includes(permission); + core.info(`Author ${username} permission: ${permission}; bypass=${canBypass}`); + core.setOutput('can-bypass', canBypass ? 'true' : 'false'); + + # ---- PR SHA pinning ---- + - name: Resolve immutable PR ref (pin to head SHA) + id: ref_comment + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/sweep') }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.issue.number; + const res = await github.rest.pulls.get({ owner, repo, pull_number: pr }); + const sha = res.data.head.sha; + core.info(`Resolved PR #${pr} head SHA: ${sha}`); + core.setOutput('ref', sha); + + - name: Reply with run link + if: ${{ github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/sweep') && github.repository_owner == 'InferenceMAX' }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + continue-on-error: true + env: + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + AUTHOR: ${{ github.event.comment.user.login }} + GEN_CMD: ${{ steps.parse.outputs.generator-args }} + CAN_BYPASS: ${{ steps.auth.outputs.can-bypass }} + PINNED_REF: ${{ steps.ref_comment.outputs.ref }} + with: + github-token: ${{ github.token }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.issue.number; + const runUrl = process.env.RUN_URL; + const author = process.env.AUTHOR; + const genCmd = process.env.GEN_CMD || ''; + const canBypass = (process.env.CAN_BYPASS || '').toLowerCase() === 'true'; + const pinned = process.env.PINNED_REF || ''; + const shortSha = pinned ? pinned.slice(0, 7) : ''; + const approvalMsg = canBypass ? 'Approval: not required (trusted collaborator).' : "Approval: required in environment 'Outside Collaborator E2E Test'."; + const body = `@${author} Kicking off a sweep.\n\nRun: ${runUrl}\nCommand: \`${genCmd}\`\nPinned ref: \`${shortSha}\`\n${approvalMsg}`; + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + + approval: + needs: get-jobs + if: ${{ github.event_name == 'issue_comment' && needs.get-jobs.outputs.pr-number != '' && needs.get-jobs.outputs.generator-args != '' && needs.get-jobs.outputs.author-can-bypass != 'true' }} + runs-on: ubuntu-latest + name: approval + environment: Outside Collaborator E2E Test + steps: + - run: echo "approved" + + validate-trusted: + needs: [get-jobs] + if: ${{ github.event_name == 'issue_comment' && needs.get-jobs.outputs.pr-number != '' && needs.get-jobs.outputs.generator-args != '' && needs.get-jobs.outputs.author-can-bypass == 'true' }} + uses: ./.github/workflows/e2e-tests.yml + name: validate (trusted author) + secrets: inherit + with: + generate-cli-command: ${{ needs.get-jobs.outputs.generator-args }} + test-name: PR #${{ needs.get-jobs.outputs.pr-number }} sweep + # Use pinned SHA to prevent TOCTOU on refs/pull//head + ref: ${{ needs.get-jobs.outputs.ref }} + + validate: + needs: [get-jobs, approval] + if: ${{ github.event_name == 'issue_comment' && needs.get-jobs.outputs.pr-number != '' && needs.get-jobs.outputs.generator-args != '' && needs.get-jobs.outputs.author-can-bypass != 'true' && needs.approval.result == 'success' }} + uses: ./.github/workflows/e2e-tests.yml + name: validate + secrets: inherit + with: + generate-cli-command: ${{ needs.get-jobs.outputs.generator-args }} + test-name: PR #${{ needs.get-jobs.outputs.pr-number }} sweep + # Use pinned SHA to prevent TOCTOU on refs/pull//head + ref: ${{ needs.get-jobs.outputs.ref }}