This is an automated email from the ASF dual-hosted git repository.
spetz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 56112a2a5 ci: add PR welcome comment, widen changes-requested gate
(#3261)
56112a2a5 is described below
commit 56112a2a5f9aec9b74b098e70abe78be9373d43e
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Thu May 14 18:47:09 2026 +0200
ci: add PR welcome comment, widen changes-requested gate (#3261)
Contributors rarely discover the triage slash commands: they
are documented in CONTRIBUTING.md but unread before a PR is
opened. On non-draft open the workflow now posts a one-time,
marker-guarded comment listing /ready, /author and
/request-review and linking the docs. It is skipped for
authors with repo write access, checked via the
collaborator-permission API rather than author_association,
which reports MEMBER for every org member on an ASF repo and
so cannot identify the core team.
Separately, a "Request changes" review from an outside
contributor was ignored: the implicit /author flip was
committer-only, so the PR stayed in S-waiting-on-review even
though the ball was back with the author. That one path is
now widened to CONTRIBUTOR and above (bots still excluded);
the /author command itself stays committer-only.
---
.github/workflows/pr-triage.yml | 120 ++++++++++++++++++++++++++++++++++++----
CONTRIBUTING.md | 8 ++-
2 files changed, 114 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/pr-triage.yml b/.github/workflows/pr-triage.yml
index d87d0431b..2ee0f9df8 100644
--- a/.github/workflows/pr-triage.yml
+++ b/.github/workflows/pr-triage.yml
@@ -30,10 +30,12 @@ name: PR Triage
# Auth gate:
# /request-review, /ready -> org MEMBER / repo COLLABORATOR/OWNER, or PR
author
# /author -> org MEMBER / repo COLLABORATOR/OWNER only
+# changes_requested review -> CONTRIBUTOR / MEMBER / COLLABORATOR / OWNER
#
# Review state (pull_request_review.submitted):
-# A changes_requested review by a committer acts as an implicit
-# /author. An explicit command in the review body overrides it.
+# A changes_requested review by a prior contributor (CONTRIBUTOR or
+# above, never a bot) acts as an implicit /author. An explicit command
+# in the review body overrides it.
#
# Feedback:
# On issue_comment commands the workflow reacts on the comment: +1 when
@@ -43,7 +45,10 @@ name: PR Triage
# have no comment to react to and stay log-only.
#
# Lifecycle (pull_request_target):
-# opened (non-draft) -> add S-waiting-on-review (only if no S-* present)
+# opened (non-draft) -> add S-waiting-on-review (only if no S-* present);
+# unless the author has repo write access, post
+# a one-time welcome comment listing the triage
+# commands (marker-guarded, best-effort)
# ready_for_review -> add S-waiting-on-review (only if no S-* present)
# converted_to_draft -> remove both S-* labels
# closed (merged or not) -> remove both S-* labels
@@ -110,8 +115,31 @@ jobs:
const LABEL_REVIEW = 'S-waiting-on-review';
const LABEL_AUTHOR = 'S-waiting-on-author';
const COMMITTER_ASSOCS = new Set(['MEMBER', 'COLLABORATOR',
'OWNER']);
+ // Wider gate for the implicit changes_requested -> author
+ // flip: a formal "Request changes" review is a deliberate,
+ // attributable action, so any prior contributor can drive
+ // it, not just committers. Derived from COMMITTER_ASSOCS so
+ // the two cannot drift. Bots are excluded separately.
+ const REVIEW_AUTHOR_ASSOCS = new Set(['CONTRIBUTOR',
...COMMITTER_ASSOCS]);
const prNumber = Number(process.env.PR_NUMBER);
+ // Welcome comment posted once when a non-draft PR is opened.
+ // The leading HTML marker is invisible in rendered Markdown
+ // and is what postWelcomeOnce scans for to stay idempotent.
+ const WELCOME_MARKER = '<!-- iggy-pr-triage-welcome -->';
+ const WELCOME_BODY = [
+ WELCOME_MARKER,
+ 'Thanks for the pull request. It is now waiting for review,
labeled `S-waiting-on-review`.',
+ '',
+ 'You can update that label as the review goes back and forth,
with slash commands - each on its own line, in a regular PR comment (not an
inline review reply):',
+ '',
+ '- `/ready` - mark it `S-waiting-on-review` again, after
addressing feedback',
+ '- `/author` - mark it `S-waiting-on-author` (maintainers only)',
+ '- `/request-review @user ...` - request reviewers (`@user` or
`@org/team`)',
+ '',
+ 'See
[CONTRIBUTING.md](https://github.com/apache/iggy/blob/master/CONTRIBUTING.md#pr-triage-commands)
for details.',
+ ].join('\n');
+
// Retry transient GitHub API failures so a single 502 cannot
// abort a command. This matters most inside setLabels: a
// remove that throws skips the paired add, and an add that
@@ -227,6 +255,58 @@ jobs:
}
};
+ // True when `login` has push (write) access to this repo --
+ // the core team, by repo/team/org grant combined. On an org
+ // repo, author_association cannot answer this: it reports
+ // MEMBER for every org member, not just repo writers, so the
+ // collaborator-permission endpoint is the only accurate
+ // signal. On failure return false -- a stray welcome on a
+ // maintainer PR is harmless, a missing one on a contributor
+ // PR defeats the feature.
+ const hasWriteAccess = async (login) => {
+ try {
+ const { data } = await withRetry(() =>
+ github.rest.repos.getCollaboratorPermissionLevel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ username: login,
+ }), `getCollaboratorPermissionLevel ${login}`);
+ return data.permission === 'write' || data.permission ===
'admin';
+ } catch (e) {
+ core.warning(`write-access check failed for ${login}:
${e.message}`);
+ return false;
+ }
+ };
+
+ // Post WELCOME_BODY once on PR open. Best-effort: a failed
+ // post must never fail the run or block labeling, the same
+ // contract as react()/reply(). Idempotent via a listComments
+ // scan for WELCOME_MARKER, so a manual workflow re-run does
+ // not double-post.
+ const postWelcomeOnce = async () => {
+ try {
+ const existing = await withRetry(() =>
github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ per_page: 100,
+ }), 'listComments');
+ if (existing.data.some(c => c.body &&
c.body.includes(WELCOME_MARKER))) {
+ core.info('welcome: already posted, skipping');
+ return;
+ }
+ await withRetry(() => github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: WELCOME_BODY,
+ }), 'welcome createComment');
+ core.info('welcome: posted');
+ } catch (e) {
+ core.warning(`welcome comment failed: ${e.message}`);
+ }
+ };
+
// ----- pull_request_target lifecycle -----
if (context.eventName === 'pull_request_target') {
const action = context.payload.action;
@@ -251,6 +331,19 @@ jobs:
core.info('lifecycle: draft PR, no label');
return;
}
+ // Welcome comment only on the initial non-draft open,
+ // and only for authors without repo write access -- the
+ // core team already knows the triage commands, the
+ // comment is contributor onboarding. ready_for_review is
+ // excluded: a PR opened as a draft and later marked ready
+ // gets no welcome, keeping scope at "non-draft open".
+ if (action === 'opened') {
+ if (await hasWriteAccess(pr.user.login)) {
+ core.info(`welcome: skipped, ${pr.user.login} has write
access`);
+ } else {
+ await postWelcomeOnce();
+ }
+ }
// Read live labels rather than the webhook-frozen
// pr.labels — a comment-driven /author or /ready may have
// landed between the lifecycle event and this run.
@@ -289,10 +382,10 @@ jobs:
const body = process.env.COMMENT_BODY || '';
// A changes_requested review is an implicit /author: the ball
- // is back with the author. Same committer-only gate as the
- // /author command (checked below, once isCommitter is known).
- // An explicit command in the review body still takes
- // precedence. payload.review is absent for issue_comment.
+ // is back with the author. Gated by REVIEW_AUTHOR_ASSOCS
+ // (checked below) -- wider than the /author command. An
+ // explicit command in the review body still takes precedence.
+ // payload.review is absent for issue_comment.
const reviewState = context.payload.review?.state;
const reviewWantsAuthor = reviewState === 'changes_requested';
@@ -317,6 +410,9 @@ jobs:
// self-flip review state.
const isBot = /\[bot\]$/.test(commentAuthor);
const isCommitter = COMMITTER_ASSOCS.has(commentAssoc) && !isBot;
+ // Permitted to drive the implicit changes_requested flip;
+ // see REVIEW_AUTHOR_ASSOCS.
+ const isReviewFlipper = REVIEW_AUTHOR_ASSOCS.has(commentAssoc) &&
!isBot;
// Live state read. payload.issue.state is the webhook-frozen
// value at delivery; comment-while-open + close-arriving-later
@@ -465,11 +561,13 @@ jobs:
// Implicit /author from a changes_requested review. Skipped
// when the review body already carried an explicit /ready or
- // /author — the command wins. Same committer-only gate as the
- // /author command.
+ // /author -- the command wins. Wider gate than the /author
+ // command: any prior contributor, not just committers (see
+ // REVIEW_AUTHOR_ASSOCS).
if (reviewWantsAuthor && !sawReady && !sawAuthor) {
- if (!isCommitter) {
- core.info(`review changes_requested: ignored, ${commentAuthor}
not a committer`);
+ if (!isReviewFlipper) {
+ core.info(`review changes_requested: ignored, ` +
+ `${commentAuthor} (${commentAssoc}) not permitted`);
} else {
await setLabels(LABEL_AUTHOR, LABEL_REVIEW);
core.info(`review changes_requested: ${LABEL_AUTHOR} <-
${LABEL_REVIEW}`);
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6e8b4336e..08aeffb29 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -163,9 +163,11 @@ for review".
### Review state
Submitting a review with "Request changes" is treated as an implicit
-`/author`: the PR moves to `S-waiting-on-author`. Only maintainers can
-trigger this, the same as the `/author` command. If your review body also
-contains an explicit `/ready` or `/author`, that command wins.
+`/author`: the PR moves to `S-waiting-on-author`. Anyone GitHub recognizes
+as a repo contributor or above can trigger this - a wider set than the
+`/author` command, which stays maintainer-only, since a formal "Request
+changes" review is a deliberate, attributable action. If your review body
+also contains an explicit `/ready` or `/author`, that command wins.
### Tips