diff --git a/.github/workflows/ci-perf.yml b/.github/workflows/ci-perf.yml index b8d2ef358..2c6f7edf9 100644 --- a/.github/workflows/ci-perf.yml +++ b/.github/workflows/ci-perf.yml @@ -21,19 +21,41 @@ on: options: - 'latest' - 'rebaseline' + pull: + description: 'Pull request number to benchmark (optional)' + required: false jobs: - tests: - name: Benchmarks + pr-benchmarks: + name: PR Benchmarks + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.pull }} runs-on: ubuntu-latest + env: + BENCHMARK_SHA: ${{ github.sha }} permissions: - contents: write + contents: read steps: - uses: actions/checkout@v6 with: - persist-credentials: true + persist-credentials: false + + - name: Checkout PR head + run: | + set -euo pipefail + PR=${{ github.event.inputs.pull }} + # Validate that PR is numeric to avoid injection or unexpected ref resolution + if ! printf '%s\n' "$PR" | grep -Eq '^[0-9]+$'; then + echo "Invalid pull request number: '$PR'. Expected a numeric value." >&2 + exit 1 + fi + # fetch the pull request head (works for forks) + git fetch origin "pull/${PR}/head:pr-${PR}" || git fetch origin "+refs/pull/${PR}/head:pr-${PR}" + git checkout "pr-${PR}" + echo "Checked out PR #${PR}" + echo "BENCHMARK_SHA=$(git rev-parse --verify HEAD)" >> "$GITHUB_ENV" + - uses: ./.github/actions/setup-action with: extensions: xdebug @@ -48,13 +70,57 @@ jobs: run: | # Baseline does not exist or rebaseline requested. Generate it. if [ -z "$(ls -A .phpbench)" ] || [ "${{ github.event.inputs.baseline || 'latest' }}" = "rebaseline" ]; then - vendor/bin/phpbench run --report=aggregate --progress=plain --store --tag=${GITHUB_SHA} + vendor/bin/phpbench run --report=aggregate --progress=plain --store --tag=${BENCHMARK_SHA} # Baseline exists. Compare against it. else - vendor/bin/phpbench run --report=aggregate --progress=plain --store --tag=${GITHUB_SHA} --ref=latest --tolerate-failure + vendor/bin/phpbench run --report=aggregate --progress=plain --store --tag=${BENCHMARK_SHA} --ref=latest --tolerate-failure fi + # Generate report for human consumption + vendor/bin/phpbench report --report=aggregate --ref=latest | + tail -n+2 | head -n-2 | tr '+' '|' > report.md + + cat report.md > "$GITHUB_STEP_SUMMARY" + + historical-benchmarks: + name: Historical Benchmarks + if: >- + github.event_name != 'workflow_dispatch' || + !github.event.inputs.pull + runs-on: ubuntu-latest + env: + BENCHMARK_SHA: ${{ github.sha }} + + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: true + + - uses: ./.github/actions/setup-action + with: + extensions: xdebug + + - name: Fetch Benchmarks + run: | + git fetch origin benchmarks + mkdir -p .phpbench + git checkout origin/benchmarks -- .phpbench || echo "No previous benchmarks found" + + - name: Run Benchmarks + run: | + # Baseline does not exist or rebaseline requested. Generate it. + if [ -z "$(ls -A .phpbench)" ] || [ "${{ github.event.inputs.baseline || 'latest' }}" = "rebaseline" ]; then + vendor/bin/phpbench run --report=aggregate --progress=plain --store --tag=${BENCHMARK_SHA} + + # Baseline exists. Compare against it. + else + vendor/bin/phpbench run --report=aggregate --progress=plain --store --tag=${BENCHMARK_SHA} --ref=latest --tolerate-failure + fi + # Generate report for human consumption vendor/bin/phpbench report --report=aggregate --ref=latest | tail -n+2 | head -n-2 | tr '+' '|' > report.md diff --git a/.github/workflows/panda.yml b/.github/workflows/panda.yml index 83359bb29..bab98f1a5 100644 --- a/.github/workflows/panda.yml +++ b/.github/workflows/panda.yml @@ -3,34 +3,77 @@ name: TheRespectPanda Bot on: issue_comment: types: [created] - if: startsWith(github.event.comment.body, '@TheRespectPanda') jobs: listen-comment: + if: startsWith(github.event.comment.body, '@TheRespectPanda') runs-on: ubuntu-latest permissions: issues: write - pull-requests: write + actions: write steps: - uses: actions/github-script@v8 with: github-token: ${{ secrets.PANDA_GITHUB_PAT }} script: | const body = (context.payload.comment && context.payload.comment.body).trim() || ''; - const usage = 'Usage: `@TheRespectPanda ping|help`'; + const usage = 'Usage: `@TheRespectPanda ping|help|benchmark`'; if (!body.startsWith('@TheRespectPanda')) { return; } + let answer; + switch (body) { case '@TheRespectPanda ping': answer = 'Pong! 🐼'; break; + case '@TheRespectPanda': case '@TheRespectPanda help': answer = 'Hello! I am TheRespectPanda Bot. ' + usage; break; + + case '@TheRespectPanda benchmark': + // Only runnable on pull requests + if (!context.payload.issue.pull_request) { + answer = 'The `benchmark` command can only be used on pull requests.'; + break; + } + + // Only members can trigger benchmarks + const association = context.payload.comment.author_association; + const allowedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; + if (!allowedAssociations.includes(association)) { + answer = 'Only repository members can trigger benchmarks.'; + break; + } + + try { + // dispatch the perf workflow + const ref = (context.payload.repository && context.payload.repository.default_branch) || 'main'; + + const workflowId = 'ci-perf.yml'; + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowId, + ref, + inputs: { + baseline: 'latest', + pull: String(context.issue.number) + } + }); + + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/${workflowId}`; + answer = `Triggered phpbench benchmarks for PR #${context.issue.number} — workflow run: ${runUrl}`; + } catch (err) { + answer = `Failed to trigger benchmarks: ${err.message}`; + } + + break; + default: answer = "I'm sorry, I don't understand that command. " + usage; }