1515# specific language governing permissions and limitations
1616# under the License.
1717
18- name : Issue Labeler
18+ name : Issue Labeler / Assigner
1919
20- # Maps the "Affected area / component" dropdown from the bug/feature issue
21- # forms (.github/ISSUE_TEMPLATE/) onto real repo labels, so reporters who
22- # lack triage permission still get their issue categorized at creation.
20+ # Two add-only triage steps that run when a reporter opens or edits an issue:
2321#
24- # Add-only by design: it never removes labels. Reporters rarely deselect,
25- # and removing would fight maintainers who labeled by hand. addLabels is
26- # idempotent, so re-running on `edited` is harmless .
22+ # 1. Label: maps the "Affected area / component" dropdown from the bug/feature
23+ # issue forms (.github/ISSUE_TEMPLATE/) onto real repo labels, so reporters
24+ # who lack triage permission still get their issue categorized at creation .
2725#
28- # SECURITY: the issue body is attacker-controlled text, but it is parsed
29- # only for exact matches against the fixed AREA_LABELS allowlist below, so
30- # the only labels this workflow can ever apply are the ones listed here.
31- # No checkout, no exec of issue contents, no token export.
26+ # 2. Assign: if the reporter ticked the "Contribution" checkbox ("I'm willing
27+ # to submit a pull request ..."), assign the issue to its author so the
28+ # volunteer is on record. addAssignees is idempotent and issue authors are
29+ # always assignable, even as outside contributors.
30+ #
31+ # Add-only by design: neither step removes anything. Reporters rarely deselect,
32+ # and removing would fight maintainers who labeled or reassigned by hand. Both
33+ # API calls are idempotent, so re-running on `edited` is harmless.
34+ #
35+ # SECURITY: the issue body is attacker-controlled text. The label step parses
36+ # it only for exact matches against the fixed AREA_LABELS allowlist below. The
37+ # assign step uses the body only as a boolean gate; the assignee comes from the
38+ # trusted `payload.issue.user.login`, never from body content, so the body
39+ # cannot inject an assignee. No checkout, no exec of issue contents, no token
40+ # export.
3241
3342on :
3443 issues :
@@ -39,7 +48,7 @@ permissions:
3948 contents : read
4049
4150jobs :
42- label :
51+ triage :
4352 runs-on : ubuntu-24.04-arm
4453 timeout-minutes : 5
4554 steps :
@@ -103,3 +112,32 @@ jobs:
103112 issue_number: context.payload.issue.number,
104113 labels,
105114 });
115+
116+ - name : Assign author who volunteered to contribute
117+ uses : actions/github-script@v9
118+ with :
119+ script : |
120+ // The "Contribution" checkbox in both issue forms renders as a
121+ // task-list line. Checked, it reads `- [x] I'm willing to submit a
122+ // pull request ...`; the tail differs per template (bug: "to fix
123+ // this bug", feature: "to implement this feature"), so match the
124+ // shared prefix only.
125+ const body = context.payload.issue.body || '';
126+ const volunteered =
127+ /(^|\n)\s*-\s*\[[xX]\]\s+I'm willing to submit a pull request/.test(body);
128+ if (!volunteered) {
129+ core.info('author did not volunteer to contribute, nothing to assign');
130+ return;
131+ }
132+
133+ // Assignee is the trusted author from the event payload, never the
134+ // body. addAssignees is a no-op if already assigned and silently
135+ // ignores non-assignable users; issue authors are always assignable.
136+ const author = context.payload.issue.user.login;
137+ core.info(`assigning volunteering author: ${author}`);
138+ await github.rest.issues.addAssignees({
139+ owner: context.repo.owner,
140+ repo: context.repo.repo,
141+ issue_number: context.payload.issue.number,
142+ assignees: [author],
143+ });
0 commit comments