Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c7b9e87
Reusable Pull Request Dashboard
trask Jun 24, 2026
1bf731d
Address pull request dashboard security findings
trask Jun 24, 2026
97edd6e
Align dashboard app token permissions
trask Jun 24, 2026
463ca04
Use dashboard app credentials for dashboard workflow
trask Jun 24, 2026
b0838da
Document dispatcher app private key secret
trask Jun 24, 2026
c9665ce
Sync dispatcher app env during Netlify deploy
trask Jun 24, 2026
f49e381
Mask dispatcher key during Netlify env sync
trask Jun 24, 2026
d90b052
Create Netlify publish directory before deploy
trask Jun 24, 2026
ddf7f53
Harden PR dashboard: drop webhook repo allowlist, escape HTML in titl…
trask Jun 24, 2026
eb9bd3e
Skip empty diagnostics section in PR dashboard
trask Jun 24, 2026
583c2ea
Address PR review: pin Node for Copilot CLI, drop misleading notifica…
trask Jun 24, 2026
bcf865a
Add trailing newline to files missing one
trask Jun 24, 2026
783acfb
Address pull request dashboard review feedback
trask Jun 24, 2026
d14d958
updates
trask Jun 24, 2026
633480c
Align dashboard checkout pins
trask Jun 24, 2026
3b9f076
Address pull request dashboard review comments
trask Jun 24, 2026
249297c
Address pull request dashboard review comments
trask Jun 24, 2026
f071eeb
Potential fix for pull request finding
trask Jun 24, 2026
d89f257
Address review comment: remove dashboard dead code
trask Jun 24, 2026
99f041e
max-parallel
trask Jun 24, 2026
2acb4fe
Isolate pull request dashboard repo runs
trask Jun 25, 2026
b846c50
Avoid inheriting all secrets in dashboard workflow
trask Jun 25, 2026
2d0202a
Create dashboard label before publishing issue
trask Jun 25, 2026
2f76162
Pass dashboard workflow secrets explicitly
trask Jun 25, 2026
69ccf9f
Revert "Pass dashboard workflow secrets explicitly"
trask Jun 25, 2026
bd65511
Remove unused dashboard trigger actor input
trask Jun 25, 2026
99067f3
Add dashboard repo workflow codeowner
trask Jun 25, 2026
fabc13d
Address pull request dashboard review comments
trask Jun 25, 2026
bf2d547
Restore Slack notification state token
trask Jun 25, 2026
4ad7918
Document reusable workflow secret handling
trask Jun 25, 2026
f48f8c6
Address dashboard review cleanups
trask Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@
# https://help.github.com/en/articles/about-code-owners

* @open-telemetry/shared-workflows-approvers

# Unlike typical shared workflows, the pull request dashboard workflow runs entirely
# in this repository.
.github/scripts/pull-request-dashboard/** @open-telemetry/shared-workflows-approvers @trask
.github/workflows/deploy-pull-request-dashboard-webhook.yml @open-telemetry/shared-workflows-approvers @trask
.github/workflows/pull-request-dashboard.yml @open-telemetry/shared-workflows-approvers @trask
.github/workflows/pull-request-dashboard-repo.yml @open-telemetry/shared-workflows-approvers @trask
2 changes: 2 additions & 0 deletions .github/scripts/pull-request-dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
.cache/
91 changes: 91 additions & 0 deletions .github/scripts/pull-request-dashboard/RATIONALE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Pull Request Dashboard Rationale

This dashboard is a maintainer aid, not a transactional notification system.
Some rare timing and notification edge cases are intentionally accepted to keep
the implementation understandable and operationally cheap.

## Central Workflow

- Full rebuilds run from `open-telemetry/shared-workflows` instead of each
target repository hosting its own workflow.
- Target repositories only need GitHub App access and an entry in
`repositories.json`.
- The top-level workflow resolves target repositories, then calls a reusable
per-repository workflow for each target. The per-repository workflow runs the
update, notification, and publishing jobs top to bottom for one repository, so
one repository's update failure does not block publishing or notifications for
repositories whose updates succeeded.
- The top-level repository matrix uses limited parallelism to reduce contention
on the shared state branch while still allowing more than one repository to
run at a time.
- State for all target repositories lives on one shared state branch, namespaced
by repository name.
- The dashboard issue is discovered dynamically by title and label, so target
repositories do not need to store issue numbers in config.

## GitHub Actions Instead Of Netlify For Full Rebuilds

- Full rebuilds are batch jobs: they read many PRs, call REST and GraphQL many
times, run Copilot classification, update git-backed state, and publish an
issue body.
- GitHub Actions provides clearer logs, concurrency controls, artifacts, and
normal retry/cancel behavior for that workload.
- Netlify remains appropriate for small webhook-sized work, but it was a poor
fit for long full-rebuild workers.

## State Branch

- Dashboard and notification state are stored on a git branch rather than in the
live dashboard issue body.
- Updates use `git push --force-with-lease`, so git refs provide the durable
compare-and-swap boundary for concurrent runs.
- Full rebuilds write the complete current state; targeted PR runs merge one PR
slot with the latest accepted state.

## GraphQL Cost

- Review threads are still fetched from GraphQL because the dashboard needs
thread-level fields such as `isResolved`, `isOutdated`, and canonical thread
grouping.
- `reviewThreads(first: 10)` is intentionally small. The nested
`comments(first: 100)` connection makes GitHub GraphQL rate-limit cost scale
with the review-thread page size.
- Pagination still fetches every review thread; the smaller page size reduces
rate-limit spikes without dropping data.

## Classification Cache

- LLM classification cache is stored with `actions/cache`.
- Cache keys are scoped by target repository and by either PR number or full
rebuild.
- Cache entries are immutable, so rolling keys plus restore prefixes pick up the
latest usable snapshot without concurrent writers overwriting each other.

## Slack Notifications

- Slack notification state is PR-granular. It does not track notification
history separately for each assignee.
- When notification state is first created, existing approver-routed PRs may
receive initial notifications on the next run. Avoiding that bootstrap case
would require storing separate seen-but-not-notified state.
- When a mapped assignee is added after a PR was already notified during the
same waiting period, that assignee may wait until the next follow-up cadence
instead of receiving an immediate initial notification.
- Slack notifications are sent only for dashboard state that has already been
accepted on the state branch. A newer dashboard update can land after the
notification job checks out state, so a notification can be slightly late
relative to the newest state.
- The notification job preserves just-written notification state across normal
state-branch CAS retries. If Slack delivery succeeds and every state-branch
push attempt is rejected, a later run can send the same notification again.
Recording state before sending Slack would avoid that duplicate window, but
could instead record notifications that were never delivered.

## Publishing

- Dashboard publishing is serialized per target repository.
- Each publish job fetches the accepted state branch while holding the publish
slot, so older jobs do not intentionally publish stale markdown over newer
accepted state.
- If another update advances the state branch while a publish job is already
editing the issue, the live issue can briefly lag until the next publish job.
83 changes: 83 additions & 0 deletions .github/scripts/pull-request-dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Pull request dashboard

This directory contains the central pull request dashboard implementation used by
`.github/workflows/pull-request-dashboard.yml` in this repository.

The workflow runs from `open-telemetry/shared-workflows` and targets repositories
listed in `repositories.json`. Target repositories do not need to host dashboard
workflow files.

See `RATIONALE.md` for the architecture and tradeoffs behind this design.
See `WEBHOOK_SETUP.md` for GitHub App webhook permissions and dispatch setup.

## Configuration

Add target repositories to `repositories.json`:

```json
[
{
"name": "example-repo",
"approver_teams": ["example-approvers"],
"required_approvals": 1,
"slack_channel": "#example-maintainers",
"slack_user_mapping": {
"octocat": "U0123456789"
}
}
]
```

The dashboard issue is discovered dynamically in the target repository by the
`dashboard` label and `Pull Request Dashboard` title. If it does not exist, the
publish step creates the label and issue.

The target repository GitHub App is installed on each configured repository.
The workflow creates repository-scoped app installation tokens with
`PR_DASHBOARD_APP_ID` and `PR_DASHBOARD_PRIVATE_KEY`, then uses those tokens for
target repository API reads/writes and approver team membership reads.

Slack notifications use the shared `SLACK_WEBHOOK_URL` secret. Each repository
can route notifications to its own `slack_channel` and map GitHub logins to
Slack user IDs via `slack_user_mapping`. Repositories without `slack_channel`
configured skip Slack notification processing.

## State

Dashboard state is stored on the shared state branch configured by
`DASHBOARD_STATE_BRANCH`. State files are namespaced by target repository, for
example:

```text
semantic-conventions-genai/dashboard-state.json
opentelemetry-java-instrumentation/dashboard-state.json
```

This allows one central workflow to manage multiple target repositories without
state collisions.

## Running

Manual run for one repository:

```text
workflow_dispatch:
repository: opentelemetry-java-instrumentation
```

Manual targeted PR refresh:

```text
workflow_dispatch:
repository: opentelemetry-java-instrumentation
pr_number: "12345"
```

Scheduled runs process every configured repository.

## Notes

- Full rebuilds run in GitHub Actions, not Netlify background functions.
- GraphQL review threads are paged in groups of 10 to reduce GraphQL point cost
while preserving the existing thread/comment data shape.
- Slack notification state remains git-backed and repository-namespaced.
169 changes: 169 additions & 0 deletions .github/scripts/pull-request-dashboard/WEBHOOK_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Pull Request Dashboard Webhook Setup

## 1. Netlify webhook bridge

Create a Netlify project for the webhook bridge:

- Repository: `open-telemetry/shared-workflows`
- Project name: `otel-pull-request-dashboard`
- Base directory: `.github/scripts/pull-request-dashboard`

The Netlify project only receives GitHub App webhooks and dispatches the central
GitHub Actions workflow. It does not run full dashboard rebuilds and does not own
dashboard state.

Save the Netlify project ID as a GitHub Actions variable named
`NETLIFY_PR_DASHBOARD_PROJECT_ID` in the `shared-workflows` repository.

Save a Netlify personal access token as a GitHub Actions secret named
`NETLIFY_AUTH_TOKEN` in the `shared-workflows` repository.

Disable Deploy Previews. PR preview deploys are unused and only add noise to
PRs. In Netlify, go to **Project configuration** -> **Build & deploy** ->
**Continuous Deployment** -> **Branches and deploy contexts**, select
**Configure**, and disable Deploy Previews.

## 2. GitHub Apps

Use two GitHub Apps to keep permissions narrow:

- a target repository app that receives target repository webhooks and grants
dashboard data access
- a shared-workflows dispatcher app that can dispatch the central workflow

### Target repository app

Create a GitHub App:

- Name: `OpenTelemetry PR Dashboard`
- Homepage URL: `https://opentelemetry.io`
- Webhook URL: `https://otel-pull-request-dashboard.netlify.app/.netlify/functions/github-webhook`

Generate and save a webhook secret:

```bash
openssl rand -hex 32
```

Repository permissions:

- Checks: read-only
- Contents: read-only
- Issues: read and write
- Metadata: read-only
- Pull requests: read-only

Organization permissions:

- Members: read-only

Permission rationale:

| Permission | Access | Why it is needed |
| ---------- | ------ | ---------------- |
| Checks | Read | Required to subscribe to check-suite events and to read check data for dashboard rows. |
| Contents | Read | Reads PR commits and repository metadata needed by pull/commit APIs. |
| Issues | Read and write | Finds/creates/updates the dashboard issue and posts review-guidance comments on PRs. |
| Metadata | Read | Required by GitHub for GitHub App repository access. |
| Pull requests | Read | Required to subscribe to PR review/comment/thread events and to read PR details, reviews, review comments, commits, and GraphQL review threads. |
| Members | Read | Reads approver-team membership configured in `repositories.json`. |

PR conversation comments are covered by `Issues: read and write`. The dashboard
does not create inline review comments, submit reviews, or resolve review
threads, so it does not need `Pull requests: write`.

Subscribe to events:

- Check suite
- Pull request
- Issue comment
- Pull request review
- Pull request review comment
- Pull request review thread

Event rationale:

| Event | Why it is needed |
| ----- | ---------------- |
| Check suite | Refreshes CI status when checks are requested, rerequested, or completed. |
| Pull request | Refreshes dashboard rows when PR state, draft status, labels, assignees, branches, or metadata change. |
| Issue comment | Refreshes PR conversation state when PR issue comments are created, edited, or deleted. |
| Pull request review | Refreshes approval/change-request state and posts guidance for submitted reviews with review comments. |
| Pull request review comment | Refreshes inline review-comment discussion state. |
| Pull request review thread | Refreshes when inline review threads are resolved or unresolved. |

Create the app, update the logo, and generate a private key.

Save the app credentials in the `shared-workflows` repository:

- GitHub Actions variable `PR_DASHBOARD_APP_ID` - target repository app ID
- GitHub Actions secret `PR_DASHBOARD_PRIVATE_KEY` - private key PEM for the
target repository app

### Shared-workflows dispatcher app

Use the [repo-specific otelbot app](https://github.com/open-telemetry/community/blob/main/assets.md#otelbot-sig-specific) for `open-telemetry/shared-workflows` to
dispatch the central workflow.

Repository permissions:

- Actions: read and write
- Metadata: read-only

This app does not need to subscribe to target repository events. It only needs
access to `open-telemetry/shared-workflows` so the webhook bridge can call the
workflow dispatch API.

## 3. Install the app

Install the target repository app on every repository listed in
`repositories.json`.

## 4. Netlify environment variables

Add this environment variable to the Netlify project for the Production deploy
context.

Secrets:

- `GITHUB_WEBHOOK_SECRET` - same webhook secret as the target repository app

The deploy workflow syncs these GitHub Actions values into the Netlify
Production function environment before deployment:

- GitHub Actions variable `OTELBOT_SHARED_WORKFLOWS_APP_ID` - repo-specific
otelbot app ID
- GitHub Actions secret `OTELBOT_SHARED_WORKFLOWS_PRIVATE_KEY` - private key PEM
for the repo-specific otelbot app that dispatches the central workflow; the
deploy workflow base64-encodes this secret before storing it in Netlify as
`OTELBOT_SHARED_WORKFLOWS_PRIVATE_KEY_BASE64`

The webhook function also supports `OTELBOT_SHARED_WORKFLOWS_PRIVATE_KEY` if the
deployment environment can store a multiline PEM value directly.

Deploy contexts:

- Production

## 5. Workflow dispatch contract

The webhook bridge should dispatch `pull-request-dashboard.yml` in
`open-telemetry/shared-workflows` with these inputs:

```json
{
"repository": "opentelemetry-java-instrumentation",
"pr_number": "12345",
"trigger_event": "pull_request_review_comment",
"trigger_action": "created",
"trigger_review_id": "67890"
}
```

Notes:

- `repository` is the short repository name under `open-telemetry`.
- Omit `pr_number` or set it to an empty string for a full rebuild.
- `trigger_review_id` is only required for `pull_request_review` `submitted`
events when review guidance may need to be posted.
- The central workflow validates these inputs before using them.
Loading