Skip to content

Pull request dashboard #200676

Pull request dashboard

Pull request dashboard #200676

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
head_sha:
description: Head commit to resolve to a pull request when the event carries no number.
required: false
type: string
trigger_event:
description: Event that requested the refresh.
required: false
type: string
permissions:
contents: read
# 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 can coalesce with generic
# PR refreshes because the live status comment no longer depends on a specific
# review id.
concurrency:
group: >-
pull-request-dashboard-${{ inputs.repository || 'all-repositories' }}-${{ inputs.pr_number || inputs.head_sha || 'backfill' }}-${{ 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
# Repositories that run dashboard code from the triggering commit, ahead of
# everyone else. Job-level `if` cannot read `env`, so the targeted canary job
# repeats this list inline; test_rollout.py keeps the two in sync.
CANARY_REPOSITORIES: '["opentelemetry-java-instrumentation", "shared-workflows"]'
jobs:
resolve-targets:
# Targeted refreshes go straight to the reusable workflow, which resolves
# its own configuration. Only backfills need a matrix resolved up front.
if: inputs.head_sha == '' && (inputs.repository == '' || inputs.pr_number == '')
runs-on: ubuntu-latest
permissions:
contents: read
environment: protected
outputs:
canary_matrix: ${{ steps.targets.outputs.canary_matrix }}
canary_count: ${{ steps.targets.outputs.canary_count }}
stable_matrix: ${{ steps.targets.outputs.stable_matrix }}
stable_count: ${{ steps.targets.outputs.stable_count }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Resolve trigger inputs
env:
TARGET_REPOSITORY_FROM_INPUT: ${{ inputs.repository }}
PR_FROM_INPUT: ${{ inputs.pr_number }}
TRIGGER_EVENT_FROM_INPUT: ${{ inputs.trigger_event }}
run: |
set -euo pipefail
if [[ -n "$PR_FROM_INPUT" ]]; then
[[ -n "$TARGET_REPOSITORY_FROM_INPUT" ]] || { echo "repository is required when pr_number is set"; exit 1; }
fi
if [[ -n "$TRIGGER_EVENT_FROM_INPUT" ]]; then
[[ "$TRIGGER_EVENT_FROM_INPUT" =~ ^(schedule|workflow_dispatch|check_suite|status|pull_request|issue_comment|pull_request_review|pull_request_review_comment|pull_request_review_thread)$ ]] \
|| { echo "bad trigger event: $TRIGGER_EVENT_FROM_INPUT"; exit 1; }
fi
- name: Resolve target repositories
id: targets
env:
TARGET_REPOSITORY: ${{ inputs.repository }}
run: |
set -euo pipefail
selected=$(jq -c --arg repo "$TARGET_REPOSITORY" '
if $repo == "" then
.
else
[ .[] | select(.name == $repo) ]
end
| [ .[] | {name} ]
' "$DASHBOARD_CONFIG")
if [[ "$selected" == "[]" ]]; then
echo "no configured repository matched: ${TARGET_REPOSITORY:-<all>}" >&2
exit 1
fi
canary=$(jq -c --argjson canary "$CANARY_REPOSITORIES" '
[ .[] | select(.name as $name | $canary | index($name) != null) ]
' <<< "$selected")
stable=$(jq -c --argjson canary "$CANARY_REPOSITORIES" '
[ .[] | select(.name as $name | $canary | index($name) == null) ]
' <<< "$selected")
{
echo "canary_matrix={\"include\":${canary}}"
echo "canary_count=$(jq 'length' <<< "$canary")"
echo "stable_matrix={\"include\":${stable}}"
echo "stable_count=$(jq 'length' <<< "$stable")"
} >> "$GITHUB_OUTPUT"
jq -n --argjson canary "$canary" --argjson stable "$stable" \
'{canary: [$canary[].name], stable: [$stable[].name]}'
resolve-head-sha:
# Check and status events for a fork head carry no pull request number, so
# the head commit has to be resolved before the reusable workflow runs.
if: inputs.head_sha != '' && inputs.pr_number == ''
runs-on: ubuntu-latest
permissions:
contents: read
environment: protected
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
channel: ${{ steps.targets.outputs.channel }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Resolve trigger inputs
id: targets
env:
TARGET_REPOSITORY: ${{ inputs.repository }}
HEAD_SHA_FROM_INPUT: ${{ inputs.head_sha }}
run: |
set -euo pipefail
[[ -n "$TARGET_REPOSITORY" ]] || { echo "repository is required when head_sha is set"; exit 1; }
[[ "$HEAD_SHA_FROM_INPUT" =~ ^[0-9a-f]{40}$ ]] || { echo "bad head SHA: $HEAD_SHA_FROM_INPUT"; exit 1; }
name=$(jq -r --arg repo "$TARGET_REPOSITORY" '
[ .[] | select(.name == $repo) ] | first | .name // empty
' "$DASHBOARD_CONFIG")
if [[ -z "$name" ]]; then
echo "no configured repository matched: $TARGET_REPOSITORY" >&2
exit 1
fi
echo "repository=$name" >> "$GITHUB_OUTPUT"
if jq -e -n --arg repo "$name" --argjson canary "$CANARY_REPOSITORIES" \
'$canary | index($repo) != null' > /dev/null; then
echo "channel=canary" >> "$GITHUB_OUTPUT"
else
echo "channel=stable" >> "$GITHUB_OUTPUT"
fi
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: head-sha-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-pull-requests: read
- name: Resolve head commit
id: trigger
env:
HEAD_SHA: ${{ inputs.head_sha }}
REPO_NAME: ${{ steps.targets.outputs.repository }}
GH_TOKEN: ${{ steps.head-sha-token.outputs.token }}
run: |
set -euo pipefail
matches=$(gh api "repos/open-telemetry/${REPO_NAME}/commits/${HEAD_SHA}/pulls" \
| jq -r --arg sha "$HEAD_SHA" '[ .[] | select(.state == "open" and .head.sha == $sha) | .number ] | sort | join(" ")')
pr_number="${matches%% *}"
if [[ -z "$pr_number" ]]; then
echo "no open pull request has head $HEAD_SHA; skipping refresh."
elif [[ "$matches" == *" "* ]]; then
# One head branch can be open against several base branches. The
# rest keep refreshing on their own events and on the backfill.
echo "head $HEAD_SHA belongs to open pull requests $matches; refreshing #$pr_number"
else
echo "resolved head $HEAD_SHA to pull request #$pr_number"
fi
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
# Rollout channels. Canary repositories run the workflow and scripts at the
# commit that triggered the run; every other repository runs them at the
# promoted rollout ref, so a change reaches the fleet only after it has soaked
# on the canary repositories.
#
# `uses` cannot take an expression, so each entry path has one job per
# channel. Promoting means cutting a release and opening a pull request that
# points every stable job at that release:
#
# uses: open-telemetry/shared-workflows/.github/workflows/pull-request-dashboard-repo.yml@<sha> # vX.Y.Z
# with:
# code_ref: <sha> # vX.Y.Z
#
# Both halves name the same commit so the scripts always match the workflow
# YAML that invoked them; test_rollout.py fails if they disagree.
run-repo-dashboard-canary:
needs: resolve-targets
if: needs.resolve-targets.outputs.canary_count != '0'
permissions:
contents: write
copilot-requests: write
strategy:
fail-fast: false
# Repositories hold separate state branches, so they only contend for the
# shared GitHub App rate limit and the Copilot CLI token. The canary set
# is small enough that one at a time keeps its added contention with the
# stable channel's slots negligible.
max-parallel: 1
matrix: ${{ fromJSON(needs.resolve-targets.outputs.canary_matrix) }}
uses: ./.github/workflows/pull-request-dashboard-repo.yml
with:
repository: ${{ matrix.name }}
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run-repo-dashboard-stable:
needs: resolve-targets
if: needs.resolve-targets.outputs.stable_count != '0'
permissions:
contents: write
copilot-requests: write
strategy:
fail-fast: false
max-parallel: 3
matrix: ${{ fromJSON(needs.resolve-targets.outputs.stable_matrix) }}
uses: open-telemetry/shared-workflows/.github/workflows/pull-request-dashboard-repo.yml@8079110d7e793bd38b1a5219ef8f8280dab9139e # v0.7.0
with:
repository: ${{ matrix.name }}
code_ref: 8079110d7e793bd38b1a5219ef8f8280dab9139e # v0.7.0
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run-targeted-dashboard-canary:
# Skipping resolve-targets removes a runner acquisition from the highest
# volume path, where webhook refreshes already know their single target.
# That is also why the canary list is inlined here instead of resolved.
if: >-
inputs.repository != '' &&
inputs.pr_number != '' &&
contains(fromJSON('["opentelemetry-java-instrumentation", "shared-workflows"]'), inputs.repository)
permissions:
contents: write
copilot-requests: write
uses: ./.github/workflows/pull-request-dashboard-repo.yml
with:
repository: ${{ inputs.repository }}
pr_number: ${{ inputs.pr_number }}
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run-targeted-dashboard-stable:
needs: run-targeted-dashboard-canary
# Reading the skip keeps the canary list in one place, and a skipped
# reusable workflow call acquires no runner.
if: >-
always() &&
inputs.repository != '' &&
inputs.pr_number != '' &&
needs.run-targeted-dashboard-canary.result == 'skipped'
permissions:
contents: write
copilot-requests: write
uses: open-telemetry/shared-workflows/.github/workflows/pull-request-dashboard-repo.yml@8079110d7e793bd38b1a5219ef8f8280dab9139e # v0.7.0
with:
repository: ${{ inputs.repository }}
pr_number: ${{ inputs.pr_number }}
code_ref: 8079110d7e793bd38b1a5219ef8f8280dab9139e # v0.7.0
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run-head-sha-dashboard-canary:
needs: resolve-head-sha
# A head commit that matches no open pull request is not a request to
# refresh the whole repository.
if: >-
needs.resolve-head-sha.outputs.pr_number != '' &&
needs.resolve-head-sha.outputs.channel == 'canary'
permissions:
contents: write
copilot-requests: write
uses: ./.github/workflows/pull-request-dashboard-repo.yml
with:
repository: ${{ inputs.repository }}
pr_number: ${{ needs.resolve-head-sha.outputs.pr_number }}
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run-head-sha-dashboard-stable:
needs: resolve-head-sha
if: >-
needs.resolve-head-sha.outputs.pr_number != '' &&
needs.resolve-head-sha.outputs.channel == 'stable'
permissions:
contents: write
copilot-requests: write
uses: open-telemetry/shared-workflows/.github/workflows/pull-request-dashboard-repo.yml@8079110d7e793bd38b1a5219ef8f8280dab9139e # v0.7.0
with:
repository: ${{ inputs.repository }}
pr_number: ${{ needs.resolve-head-sha.outputs.pr_number }}
code_ref: 8079110d7e793bd38b1a5219ef8f8280dab9139e # v0.7.0
secrets:
PR_DASHBOARD_PRIVATE_KEY: ${{ secrets.PR_DASHBOARD_PRIVATE_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
notify-hourly-failure:
needs:
- resolve-targets
- run-repo-dashboard-canary
- run-repo-dashboard-stable
# 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:
# GitHub replaces an older pending per-repo publisher even with
# cancel-in-progress disabled. Its replacement drains all durable work,
# while genuine matrix failures take precedence over cancellation. A
# channel with no repositories is skipped, which is not a failure.
success: ${{ needs.resolve-targets.result == 'success' && (needs.run-repo-dashboard-canary.result == 'success' || needs.run-repo-dashboard-canary.result == 'cancelled' || needs.run-repo-dashboard-canary.result == 'skipped') && (needs.run-repo-dashboard-stable.result == 'success' || needs.run-repo-dashboard-stable.result == 'cancelled' || needs.run-repo-dashboard-stable.result == 'skipped') }}