Skip to content

Pull request dashboard #31879

Pull request dashboard

Pull request dashboard #31879

name: Pull request dashboard
on:
schedule:
- cron: "0 * * * *" # hourly
workflow_dispatch:
inputs:
repository:
description: Target repository. Empty means all configured repositories.
required: false
type: string
pr_number:
description: Pull request number to refresh. Empty means backfill.
required: false
type: string
trigger_event:
description: Event that requested the refresh.
required: false
type: string
trigger_action:
description: Event action that requested the refresh.
required: false
type: string
trigger_review_id:
description: Pull request review id that requested the refresh.
required: false
type: string
permissions:
contents: read
# resolve-targets normally finishes in seconds, but during organization-wide
# runner queueing, redundant webhook runs can accumulate into a large backlog
# before job-level concurrency applies.
#
# Dashboard state is loaded live, so only the latest pending webhook refresh is
# needed. Manual runs remain separate because they can refresh large
# repositories that webhooks skip. Submitted reviews remain distinct because
# their review id is required to post review guidance.
concurrency:
group: >-
pull-request-dashboard-${{ inputs.repository || 'all-repositories' }}-${{ inputs.pr_number || 'backfill' }}-${{ inputs.trigger_event == 'pull_request_review' && inputs.trigger_action == 'submitted' && inputs.trigger_review_id || github.event_name == 'workflow_dispatch' && (inputs.trigger_event == '' || inputs.trigger_event == 'workflow_dispatch') && 'manual' || 'refresh' }}
cancel-in-progress: false
env:
DASHBOARD_CONFIG: .github/scripts/pull-request-dashboard/repositories.json
jobs:
resolve-targets:
runs-on: cncf-ubuntu-2-8-x86
permissions:
contents: read
environment: protected
outputs:
matrix: ${{ steps.targets.outputs.matrix }}
pr_number: ${{ steps.trigger.outputs.pr_number }}
trigger_event: ${{ steps.trigger.outputs.event }}
trigger_action: ${{ steps.trigger.outputs.action }}
trigger_review_id: ${{ steps.trigger.outputs.review_id }}
dashboard_precondition_met: ${{ steps.dashboard-precondition.outputs.met }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Resolve trigger inputs
id: trigger
env:
EVENT_NAME: ${{ github.event_name }}
TARGET_REPOSITORY_FROM_INPUT: ${{ inputs.repository }}
PR_FROM_INPUT: ${{ inputs.pr_number }}
TRIGGER_EVENT_FROM_INPUT: ${{ inputs.trigger_event }}
TRIGGER_ACTION_FROM_INPUT: ${{ inputs.trigger_action }}
TRIGGER_REVIEW_ID_FROM_INPUT: ${{ inputs.trigger_review_id }}
run: |
set -euo pipefail
pr_number=""
trigger_event=""
trigger_action=""
trigger_review_id=""
case "$EVENT_NAME" in
schedule)
trigger_event="$EVENT_NAME"
;;
workflow_dispatch)
pr_number="$PR_FROM_INPUT"
trigger_event="${TRIGGER_EVENT_FROM_INPUT:-workflow_dispatch}"
trigger_action="$TRIGGER_ACTION_FROM_INPUT"
trigger_review_id="$TRIGGER_REVIEW_ID_FROM_INPUT"
;;
esac
if [[ -n "$pr_number" ]]; then
[[ "$pr_number" =~ ^[1-9][0-9]{0,6}$ ]] || { echo "bad PR number: $pr_number"; exit 1; }
[[ -n "$TARGET_REPOSITORY_FROM_INPUT" ]] || { echo "repository is required when pr_number is set"; exit 1; }
fi
if [[ -n "$trigger_event" ]]; then
[[ "$trigger_event" =~ ^(schedule|workflow_dispatch|check_suite|pull_request|issue_comment|pull_request_review|pull_request_review_comment|pull_request_review_thread)$ ]] \
|| { echo "bad trigger event: $trigger_event"; exit 1; }
fi
if [[ -n "$trigger_action" ]]; then
[[ "$trigger_action" =~ ^[a-z_]{1,32}$ ]] || { echo "bad trigger action: $trigger_action"; exit 1; }
fi
if [[ -n "$trigger_review_id" ]]; then
[[ "$trigger_review_id" =~ ^[1-9][0-9]{0,19}$ ]] || { echo "bad review id: $trigger_review_id"; exit 1; }
fi
{
echo "pr_number=$pr_number"
echo "event=$trigger_event"
echo "action=$trigger_action"
echo "review_id=$trigger_review_id"
} >> "$GITHUB_OUTPUT"
- name: Resolve target repositories
id: targets
env:
TARGET_REPOSITORY: ${{ inputs.repository }}
run: |
set -euo pipefail
matrix=$(jq -c --arg repo "$TARGET_REPOSITORY" '
if $repo == "" then
.
else
[ .[] | select(.name == $repo or ("open-telemetry/" + .name) == $repo) ]
end
' "$DASHBOARD_CONFIG")
if [[ "$matrix" == "[]" ]]; then
echo "no configured repository matched: ${TARGET_REPOSITORY:-<all>}" >&2
exit 1
fi
echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT"
echo "repository=$(jq -r '.[0].name' <<< "$matrix")" >> "$GITHUB_OUTPUT"
echo "$matrix" | jq .
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
if: steps.trigger.outputs.pr_number != ''
id: dashboard-token
with:
client-id: ${{ vars.PR_DASHBOARD_CLIENT_ID }}
private-key: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
owner: open-telemetry
repositories: ${{ steps.targets.outputs.repository }}
permission-issues: read
- name: Check dashboard precondition
id: dashboard-precondition
env:
GH_TOKEN: ${{ steps.dashboard-token.outputs.token }}
TRIGGER_PR_NUMBER: ${{ steps.trigger.outputs.pr_number }}
REPO: open-telemetry/${{ steps.targets.outputs.repository }}
run: |
set -euo pipefail
if [[ -z "$TRIGGER_PR_NUMBER" ]]; then
echo "met=true" >> "$GITHUB_OUTPUT"
exit 0
fi
dashboard_exists=$(python3 .github/scripts/pull-request-dashboard/publish_dashboard.py --repo "$REPO" --check-dashboard-exists)
if [[ "$dashboard_exists" == "true" ]]; then
echo "met=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$dashboard_exists" == "false" ]]; then
echo "Dashboard issue does not exist yet; skipping targeted refresh."
echo "met=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "unexpected dashboard existence result: $dashboard_exists" >&2
exit 1
run-repo-dashboard:
needs: resolve-targets
if: needs.resolve-targets.outputs.dashboard_precondition_met == 'true'
permissions:
contents: write
strategy:
fail-fast: false
max-parallel: 1
matrix: ${{ fromJSON(needs.resolve-targets.outputs.matrix) }}
uses: ./.github/workflows/pull-request-dashboard-repo.yml
with:
repository: ${{ matrix.name }}
pr_number: ${{ needs.resolve-targets.outputs.pr_number }}
trigger_event: ${{ needs.resolve-targets.outputs.trigger_event }}
trigger_action: ${{ needs.resolve-targets.outputs.trigger_action }}
trigger_review_id: ${{ needs.resolve-targets.outputs.trigger_review_id }}
required_approvals: ${{ matrix.required_approvals || 1 }}
approver_teams_json: ${{ toJSON(matrix.approver_teams || fromJSON('[]')) }}
slack_channel: ${{ matrix.slack_channel }}
slack_user_mapping_json: ${{ toJSON(matrix.slack_user_mapping || fromJSON('{}')) }}
large_repo: ${{ matrix.large_repo || false }}
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
notify-hourly-failure:
needs:
- resolve-targets
- run-repo-dashboard
# Notify for backfills: scheduled runs and unfiltered manual runs.
if: >-
always() &&
(
github.event_name == 'schedule' ||
(
github.event_name == 'workflow_dispatch' &&
inputs.repository == '' &&
inputs.pr_number == ''
)
)
permissions:
issues: write # needed to open/close the hourly failure tracking issue
uses: ./.github/workflows/workflow-failure-issue.yml
with:
# A superseded per-repo publish is cancelled (publish-dashboard uses
# cancel-in-progress: true), so count cancelled as success here to avoid
# opening noisy failure issues. Genuine failures still surface because
# matrix aggregation reports 'failure' in preference to 'cancelled'.
success: ${{ needs.resolve-targets.result == 'success' && (needs.run-repo-dashboard.result == 'success' || needs.run-repo-dashboard.result == 'cancelled') }}