name: 'PVF Trigger' description: 'On a /pvf PR comment, resolve the candidate from the latest Build run and dispatch the host workflow (performance-validation.yml by default), in this repo or a separate dashboard-host repo. The candidate is a plugin version recorded by prepare-pvf (default), or an uploaded build artifact such as a native subprocess when executable-artifact is set.' inputs: comment: description: 'Comment body to scan for a /pvf command' required: true pr-number: description: 'Pull request number the comment was posted on' required: true build-workflow: description: 'Build workflow to inspect. Resolves candidate-version unless executable-artifact is set, then resolves that artifact.' required: false default: build.yml executable-artifact: description: 'Name of a build artifact to validate instead of a plugin version. Empty (default) uses the candidate-version model.' required: false default: '' rule-prefixes: description: 'Space-separated rule-key prefixes passed to the /pvf comment parser' required: false default: 'S' target-repo: description: 'owner/name of the repo that hosts performance-validation.yml. Defaults to the current repo (same-repo dispatch). Set to a separate private/internal repo to publish an SSO-gated dashboard while this repo stays public.' required: false default: ${{ github.repository }} target-ref: description: 'Git ref to dispatch performance-validation.yml on in target-repo. Empty falls back to the PR head ref (same-repo behaviour); set to the target repo default branch (e.g. master) for cross-repo dispatch.' required: false default: '' dispatch-token: description: 'Token used ONLY for the cross-repo workflow dispatch (needs actions:write on target-repo). Empty falls back to github.token, which only works for same-repo dispatch.' required: false default: '' target-workflow: description: 'Filename of the workflow to dispatch in target-repo. Lets one host repo serve several analyzers via per-analyzer host workflows. Empty falls back to performance-validation.yml.' required: false default: performance-validation.yml baseline-version: description: 'Forwarded as the baseline-version input of the host workflow, e.g. DEV. Empty omits it, so the host workflow default applies. The host workflow must declare the input.' required: false default: '' use-fast-run: description: 'Forwarded as the use-fast-run input of the host workflow (true/false). Empty omits it, so the host workflow default applies. The host workflow must declare the input.' required: false default: '' runs: using: 'composite' steps: - name: Parse /pvf comment id: pvf uses: SonarSource/core-languages-tooling-public/pvf-comment@master with: comment: ${{ inputs.comment }} rule-prefixes: ${{ inputs.rule-prefixes }} - name: Trigger performance validation id: trigger if: steps.pvf.outputs.found == 'true' shell: bash env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ inputs.pr-number }} RULES_REQUEST: ${{ steps.pvf.outputs.rules-request }} BUILD_WORKFLOW: ${{ inputs.build-workflow }} EXECUTABLE_ARTIFACT: ${{ inputs.executable-artifact }} TARGET_REPO: ${{ inputs.target-repo }} TARGET_REF: ${{ inputs.target-ref }} DISPATCH_TOKEN: ${{ inputs.dispatch-token }} TARGET_WORKFLOW: ${{ inputs.target-workflow }} BASELINE_VERSION: ${{ inputs.baseline-version }} USE_FAST_RUN: ${{ inputs.use-fast-run }} run: | set -euo pipefail fail() { # A reason spanning several lines would corrupt GITHUB_OUTPUT. local reason="${1//$'\n'/ }" echo "reason=${reason}" >> "$GITHUB_OUTPUT" echo "$reason" >&2 exit 1 } trap 'fail "unexpected error on line ${LINENO}"' ERR pull="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" head_ref="$(jq -r .head.ref <<<"$pull")" head_sha="$(jq -r .head.sha <<<"$pull")" title="$(jq -r .title <<<"$pull")" # Use the newest run containing the required artifact. Each model forwards different inputs. run_ids="$(gh api \ "repos/${GITHUB_REPOSITORY}/actions/workflows/${BUILD_WORKFLOW}/runs?head_sha=${head_sha}&event=pull_request&per_page=10" \ --jq '.workflow_runs[].id')" \ || fail "Failed to list ${BUILD_WORKFLOW} runs for ${head_sha}" resolved_run_id="" if [ -n "${EXECUTABLE_ARTIFACT}" ]; then for run_id in $run_ids; do has="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" \ | jq --arg name "$EXECUTABLE_ARTIFACT" 'any(.artifacts[]; .name == $name and .expired == false)')" \ || continue if [ "$has" = "true" ]; then resolved_run_id="$run_id" break fi done [ -n "$resolved_run_id" ] \ || fail "No ${EXECUTABLE_ARTIFACT} artifact for ${head_sha}; wait for Build to finish" candidate_args=(-f "uploaded-executable-run-id=${resolved_run_id}" -f "executable-artifact=${EXECUTABLE_ARTIFACT}") else for run_id in $run_ids; do if gh run download "$run_id" --repo "$GITHUB_REPOSITORY" -n candidate-version; then resolved_run_id="$run_id" break fi done [ -n "$resolved_run_id" ] \ || fail "No candidate-version artifact for ${head_sha}; wait for Build to finish" [ -s candidate-version.txt ] \ || fail "candidate-version artifact for ${head_sha} is missing or empty" candidate_args=(-f "candidate-version=$(cat candidate-version.txt)") fi TARGET_REPO="${TARGET_REPO:-$GITHUB_REPOSITORY}" token="${DISPATCH_TOKEN:-$GH_TOKEN}" workflow="${TARGET_WORKFLOW:-performance-validation.yml}" extra_args=() if [ "$TARGET_REPO" != "$GITHUB_REPOSITORY" ]; then if [ -z "$TARGET_REF" ]; then fail "target-ref is required for cross-repo dispatch to ${TARGET_REPO}; set it to the host repo default branch (e.g. master)" fi ref="$TARGET_REF" extra_args+=(-f "analyzer-repo=${GITHUB_REPOSITORY}") else # Same-repo dispatch: ref defaults to the PR head ref. ref="${TARGET_REF:-$head_ref}" fi if [ -n "$BASELINE_VERSION" ]; then extra_args+=(-f "baseline-version=${BASELINE_VERSION}") fi if [ -n "$USE_FAST_RUN" ]; then extra_args+=(-f "use-fast-run=${USE_FAST_RUN}") fi if ! dispatch_output="$(GH_TOKEN="$token" gh workflow run "$workflow" \ --repo "$TARGET_REPO" \ --ref "$ref" \ "${candidate_args[@]}" \ -f "rules-request=${RULES_REQUEST}" \ -f "pr-number=${PR_NUMBER}" \ -f "run-description=${title}" \ ${extra_args[@]+"${extra_args[@]}"} 2>&1)"; then fail "${TARGET_REPO} rejected the dispatch of ${workflow} on ${ref}: ${dispatch_output}" fi echo "$dispatch_output" - name: Acknowledge the /pvf comment if: steps.pvf.outputs.found == 'true' shell: bash env: GH_TOKEN: ${{ github.token }} COMMENT_ID: ${{ github.event.comment.id }} run: | set -euo pipefail gh api --method POST \ "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}/reactions" \ -f content=eyes - name: Report the /pvf failure if: failure() && steps.trigger.outcome != 'success' shell: bash env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ inputs.pr-number }} COMMENT_AUTHOR: ${{ github.event.comment.user.login }} REASON: ${{ steps.trigger.outputs.reason }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | set -euo pipefail body="@${COMMENT_AUTHOR} ❌ Your \`/pvf\` command was not dispatched. [See the failed run](${RUN_URL})." body+=$'\n\n'"> ${REASON:-unexpected failure}" gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "$body"