Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c0f48d
PR meta: Move the props list into the shared comment
manzoorwanijk Sep 1, 2026
9b92aac
PR meta: Harden the props writer and its heading demotion
manzoorwanijk Sep 1, 2026
52ceb5d
PR meta: Order props writes by run, and key props runs by pull request
manzoorwanijk Sep 1, 2026
ed60417
PR meta: Drop the run ordering
manzoorwanijk Sep 25, 2026
5dfd1b6
PR meta: Stop folding the flaky tests, and widen the props budget
manzoorwanijk Sep 25, 2026
fee0bc8
PR meta: Close a truncated fence with the marker that opened it
manzoorwanijk Sep 25, 2026
2211c61
PR meta: Fix the props writer's inputs and cancellation
manzoorwanijk Sep 25, 2026
577f1ab
PR meta: Keep a no-op label run out of the props group
manzoorwanijk Sep 25, 2026
051c2ac
PR meta: Keep runs that cannot write out of the props group
manzoorwanijk Sep 28, 2026
a9bacd2
PR meta: Keep the props trailer when the list is truncated
manzoorwanijk Sep 28, 2026
0588f8a
PR meta: Remove the label before the gather starts, not beside it
manzoorwanijk Sep 28, 2026
bcb97c7
PR meta: Keep a label run out of the shared group
manzoorwanijk Sep 28, 2026
e790d81
PR meta: Make the truncation test catch the case it was written for
manzoorwanijk Sep 28, 2026
04fec89
PR meta: Make the writer's condition explicit, and cut inside a trailer
manzoorwanijk Sep 29, 2026
a2bef71
PR meta: Take the label out of the props workflow, and stop re-trunca…
manzoorwanijk Sep 30, 2026
b006562
PR meta: Measure a truncation instead of reserving room for it
manzoorwanijk Sep 30, 2026
66f73e9
PR meta: Keep a run link from breaking the budget it is measured against
manzoorwanijk Sep 30, 2026
1af19a7
PR meta: Bound the run link where it is stored, not only where it is …
manzoorwanijk Sep 30, 2026
7f774f9
PR meta: Take only a marker attribute that reads like what it claims
manzoorwanijk Sep 30, 2026
7932fe0
PR meta: Accept a run link without TLS
manzoorwanijk Sep 30, 2026
7533a52
PR meta: Say in the comment that editing it notifies nobody
manzoorwanijk Sep 30, 2026
8bb5825
Merge branch 'trunk' into add/pr-meta-props-section
manzoorwanijk Sep 30, 2026
84c9ca4
PR meta: Keep an unrelated label out of the props group again
manzoorwanijk Sep 30, 2026
eb74029
PR meta: Say which order a release branch takes the change in
manzoorwanijk Sep 30, 2026
116240c
PR meta: Hold a section back until the target branch can carry it
manzoorwanijk Sep 30, 2026
c5c37d1
Merge branch 'trunk' into add/pr-meta-props-section
ciampo Sep 30, 2026
b5d576c
Props Bot: Preserve standalone updates on release branches
ciampo Sep 30, 2026
4e1bc5b
Props Bot: Pin the action to a verified commit
ciampo Sep 30, 2026
b239a79
Merge branch 'trunk' into add/pr-meta-props-section
manzoorwanijk Oct 1, 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
55 changes: 55 additions & 0 deletions .github/workflows/props-bot-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Props Bot Label

on:
# Removing the label is what lets it be added again to ask for another
# refresh, so it lives apart from the gather: that one cancels superseded
# runs, and a cancellation here would strand the label.
pull_request_target:
types:
- labeled
# A label added while the pull request could not be acted on is a
# request still waiting, so these consume it when it can be.
- ready_for_review
- reopened

# Disable permissions for all available scopes by default.
# Any needed permissions should be configured at the job level.
Comment thread
manzoorwanijk marked this conversation as resolved.
Dismissed
permissions: {}

jobs:
remove-label:
name: Remove the props-bot label
runs-on: 'ubuntu-24.04'
permissions:
pull-requests: write
timeout-minutes: 5
# Matches the gather's own guard: removing the label on a draft would
# take away the request without anything acting on it.
if: |
(
github.event.action == 'labeled' && 'props-bot' == github.event.label.name ||
github.event.action != 'labeled' && contains( github.event.pull_request.labels.*.name, 'props-bot' )
) &&
! github.event.pull_request.draft && github.event.pull_request.state == 'open'

steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ISSUE_NUMBER: ${{ github.event.number }}
with:
retries: 2
retry-exempt-status-codes: 418
script: |
try {
await github.rest.issues.removeLabel( {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: process.env.ISSUE_NUMBER,
name: 'props-bot',
} );
} catch ( error ) {
// Someone removed it first, which is the same outcome.
if ( error.status !== 404 ) {
throw error;
}
}
110 changes: 85 additions & 25 deletions .github/workflows/props-bot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,62 +29,122 @@ on:
types:
- created

# Cancels all previous workflow runs for pull requests that have not completed.
# Cancels all previous runs for a pull request that have not completed, which
# is also what keeps two gathers from writing their lists out of order.
concurrency:
# The concurrency group contains the workflow name and the branch name for pull requests
# or the commit hash for any other events.
group: ${{ github.workflow }}-${{ contains( fromJSON( '["pull_request_target", "pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && github.head_ref || github.sha }}
# Keyed by the pull request, which the four events carry in three different
# places. A fork's review run stands apart: its token cannot write, so
# sharing the key would let it cancel a run that could have.
group: ${{ github.workflow }}-${{ ( ! startsWith( github.event_name, 'pull_request_review' ) || github.event.pull_request.head.repo.full_name == github.repository ) && ( github.event.number || github.event.issue.number || github.event.pull_request.number ) || github.run_id }}
cancel-in-progress: true
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved

# Disable permissions for all available scopes by default.
# Any needed permissions should be configured at the job level.
permissions: {}

jobs:
# Compiles a list of props for a pull request.
#
# Performs the following steps:
# - Collects a list of contributor props and leaves a comment.
# - Removes the props-bot label, if necessary.
# Collects a list of contributor props for the PR meta comment to post.
props-bot:
name: Generate a list of props
runs-on: 'ubuntu-24.04'
permissions:
# The action needs permission `write` permission for PRs in order to add a comment.
pull-requests: write
# Read is enough: the action posts nothing, see `post-comment`.
pull-requests: read
contents: read
timeout-minutes: 20
outputs:
comment-body: ${{ steps.props.outputs.comment-body }}
# The job will run when pull requests are open, ready for review and:
#
# - A comment is added to the pull request.
# - A review is created or commented on.
# - The pull request is opened, synchronized, marked ready for review, or reopened.
# - The `props-bot` label is added to the pull request.
# A fork's review run is skipped rather than gathered and thrown away:
# its token cannot write the comment at the end of it.
if: |
(
github.event_name == 'issue_comment' && github.event.issue.pull_request ||
contains( fromJSON( '["pull_request_review", "pull_request_review_comment"]' ), github.event_name ) ||
contains( fromJSON( '["pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && github.event.pull_request.head.repo.full_name == github.repository ||
github.event_name == 'pull_request_target' && github.event.action != 'labeled' ||
'props-bot' == github.event.label.name
) &&
( ! github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open' )

steps:
- name: Gather a list of contributors
id: props
uses: WordPress/props-bot-action@trunk
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
with:
# The PR meta comment carries the list, so the action renders
# it to an output and posts nothing itself.
post-comment: false
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved

pr-meta:
name: Update PR meta comment
needs: props-bot
# Only when the list was gathered: an empty body from a failed run would
# clear a props list nothing had disproved.
if: ${{ needs.props-bot.result == 'success' }}
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved
runs-on: 'ubuntu-24.04'
permissions:
contents: read
pull-requests: write
timeout-minutes: 5
# Shared with every other writer of this comment. See bundle-size.yml.
# This workflow runs on four events, and only some of them carry the
# pull request at the same place in the payload.
concurrency:
group: pr-meta-${{ github.event.number || github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
queue: max

- name: Remove the props-bot label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
if: ${{ github.event.action == 'labeled' && 'props-bot' == github.event.label.name }}
steps:
# This job holds a writable token, and the review events check out
# `refs/pull/N/merge` by default, which carries the pull request's
# own code. Pin the action to the default branch so a pull request
# cannot supply the code that runs here.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: tools/pr-meta
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
persist-credentials: false

- name: Render the props list
env:
ISSUE_NUMBER: ${{ github.event.number }}
# Through the environment, never interpolated into the script.
BODY: ${{ needs.props-bot.outputs.comment-body }}
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved
run: |
mkdir -p pr-meta
: > pr-meta/body.md

# No contributors, so an empty body clears the section rather
# than leaving one that holds nothing but the note.
if [ -z "$BODY" ]; then
exit 0
fi

# Drop the intro wherever it sits: it is what the standalone
# props bot searches for, and it would find this comment. Its
# note about refreshing goes back in wording that will not.
printf '%s\n' "$BODY" | sed '/^The following accounts have interacted/d' > pr-meta/body.md

cat >> pr-meta/body.md <<'EOF'

_Updated as activity occurs. Add the `props-bot` label to refresh._
EOF

- uses: ./tools/pr-meta
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved
with:
retries: 2
retry-exempt-status-codes: 418
script: |
github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: process.env.ISSUE_NUMBER,
name: 'props-bot'
});
repo-token: ${{ secrets.GITHUB_TOKEN }}
section: props
body-path: pr-meta/body.md
# The action comes from the default branch, which need not
# yet know where each of the four events carries this.
pr-number: ${{ github.event.number || github.event.issue.number || github.event.pull_request.number }}
# Retires the standalone comment on pull requests open before
# this landed, so no one copies its list once it stops moving.
# Editing a comment notifies nobody, so an unlinked
# contributor is no longer pinged by this list.
retire-comments-matching: 'The following accounts have interacted'
14 changes: 12 additions & 2 deletions tools/pr-meta/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,25 @@ permissions:
pull-requests: write
```

**`if: ${{ !cancelled() }}`**, so a failed producer still clears its section instead of leaving a stale one behind.
**A deliberate answer to producer failure.** Use `if: ${{ !cancelled() }}` where a missing result reliably means "nothing to report", so a failed producer clears its section rather than leaving a stale one. Where a missing result is instead indistinguishable from a failure, require the producer to have succeeded, as the props and flaky tests writers do; clearing there would erase something nothing had disproved.

The job also needs `actions/checkout` before `uses: ./tools/pr-meta`, since a local action needs the repository on disk. `sparse-checkout: tools/pr-meta` is enough. Under `pull_request_target` the checkout must stay on the base ref, never the pull request's head.

## What a section gives up

The comment is edited in place, and GitHub only notifies on the `@mention`s in a comment when it is first created. A section that mentions someone therefore does not notify them, so it cannot be the only way they hear about something.

A writer running an older revision of this action renders every section from what it reads back, so it shows a section it does not know about verbatim rather than reformatting it. Release branches can lag trunk for a while, and that is what keeps them from reshaping a section they were never taught.
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved

## Adding a section

Add it to `SECTIONS` in `src/sections.ts` with an id, heading, scope and character budget. Headings lead with an emoji, so a reader scanning a comment of seven sections can find theirs without reading any of them. The budgets must sum, with the headings and markers, to less than GitHub's 65536-character comment limit; a test covers that.
Comment thread
manzoorwanijk marked this conversation as resolved.
Resolved

A `summary` collapses the section behind a fold labelled with it, for content long enough that it would otherwise push the rest of the comment out of view. Leave it out to keep the section open.
A producer renders its markdown without knowing where it will sit, so any headings in a body are demoted to sit below the section heading, keeping their relative hierarchy. Headings inside a code fence are left alone, and a body deep enough to need a seventh level flattens at the sixth, markdown having no more. Setext headings, the ones underlined with `=` or `-`, are not demoted.

A `summary` collapses the section behind a fold labelled with it, for content long enough that it would otherwise push the rest of the comment out of view. Leave it out to keep the section open, which is what a body that folds its own items already needs.

`keep` decides which end survives truncation. The default drops the ending, which suits a section whose first lines matter most. `keep: 'end'` drops the beginning instead, for a body like props that closes with the part a reader acts on, and reopens a code fence the dropped start left open.

`scope` decides how staleness is handled. `commit` sections describe one commit, carry its SHA, and are rejected if they arrive from a rerun of an older one. `pr-state` sections describe the pull request as it currently is and carry no SHA.

Expand Down
3 changes: 3 additions & 0 deletions tools/pr-meta/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ inputs:
run-url:
description: 'URL of the workflow run that produced the content.'
required: false
retire-comments-matching:
description: 'Text identifying a standalone comment this section replaces. Any the bot posted are deleted once the section is written.'
required: false
runs:
# Node 24 strips the types on the fly, so the action runs straight from its
# TypeScript sources without a build step. This requires every relative
Expand Down
Loading
Loading