-
-
Notifications
You must be signed in to change notification settings - Fork 89
[feature/releaser] Automate assigning/unassigning issues #571 #572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| name: Issue Assignment Bot | ||
|
|
||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
|
|
||
| permissions: | ||
| issues: write | ||
|
|
||
| jobs: | ||
| respond-to-assign-request: | ||
| runs-on: ubuntu-latest | ||
| if: | | ||
| github.event.issue.pull_request == null && | ||
| ( | ||
| contains(toLower(github.event.comment.body), 'assign this issue to me') || | ||
| contains(toLower(github.event.comment.body), 'assign me') || | ||
| contains(toLower(github.event.comment.body), 'can i work on this') || | ||
| contains(toLower(github.event.comment.body), 'i would like to work on this') || | ||
| contains(toLower(github.event.comment.body), 'i want to work on this') | ||
| ) | ||
| steps: | ||
| - name: Respond to assignment request | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const contributingUrl = 'https://openwisp.io/docs/developer/contributing.html'; | ||
| const commenter = context.payload.comment.user.login; | ||
|
|
||
| const message = `Hi @${commenter} 👋, | ||
|
|
||
| Thank you for your interest in contributing to OpenWISP! 🎉 | ||
|
|
||
| According to our [contributing guidelines](${contributingUrl}), **you don't need to wait to be assigned** to start working on an issue. We encourage you to: | ||
|
|
||
| 1. **Fork the repository** and start working on your solution | ||
| 2. **Open a Pull Request (PR) as soon as possible** - even as a draft if it's still in progress | ||
| 3. **Link your PR to this issue** by including \`Fixes #${context.payload.issue.number}\` in the PR description | ||
|
|
||
| Once you open a PR that references this issue, you will be automatically assigned to it. | ||
|
|
||
| This approach helps us: | ||
| - See your progress and provide early feedback | ||
| - Avoid multiple contributors working on the same issue unknowingly | ||
| - Keep the contribution process moving smoothly | ||
|
|
||
| We look forward to your contribution! If you have any questions, feel free to ask in the PR or check our [documentation](${contributingUrl}). | ||
|
|
||
| Happy coding! 🚀`; | ||
|
|
||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.issue.number, | ||
| body: message | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| name: PR Issue Auto-Assignment | ||
|
||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened] | ||
|
|
||
| permissions: | ||
| issues: write | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
| auto-assign-issue: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Auto-assign linked issues to PR author | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const prBody = context.payload.pull_request.body || ''; | ||
| const prAuthor = context.payload.pull_request.user.login; | ||
| const prNumber = context.payload.pull_request.number; | ||
| const issuePattern = /(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi; | ||
| const matches = [...prBody.matchAll(issuePattern)]; | ||
| if (matches.length === 0) { | ||
| console.log('No linked issues found in PR body'); | ||
| return; | ||
| } | ||
| const MAX_ISSUES_TO_PROCESS = 10; | ||
| if (matches.length > MAX_ISSUES_TO_PROCESS) { | ||
| console.log(`Found ${matches.length} issue references, processing first ${MAX_ISSUES_TO_PROCESS} to avoid rate limits`); | ||
| } | ||
| const processedIssues = new Set(); | ||
| for (const match of matches) { | ||
| const issueNumber = parseInt(match[1], 10); | ||
| if (processedIssues.has(issueNumber)) { | ||
| continue; | ||
| } | ||
| if (processedIssues.size >= MAX_ISSUES_TO_PROCESS) { | ||
| console.log(`Reached limit of ${MAX_ISSUES_TO_PROCESS} issues, stopping processing`); | ||
| break; | ||
| } | ||
|
|
||
| processedIssues.add(issueNumber); | ||
| try { | ||
| const issue = await github.rest.issues.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber | ||
| }); | ||
| if (issue.data.pull_request) { | ||
| console.log(`#${issueNumber} is a pull request, skipping`); | ||
| continue; | ||
| } | ||
| if (issue.data.assignees && issue.data.assignees.length > 0) { | ||
| const currentAssignees = issue.data.assignees.map(a => a.login); | ||
| if (currentAssignees.includes(prAuthor)) { | ||
| console.log(`Issue #${issueNumber} already assigned to ${prAuthor}`); | ||
| continue; | ||
| } | ||
| console.log(`Issue #${issueNumber} already assigned to: ${currentAssignees.join(', ')}`); | ||
| continue; | ||
| } | ||
| await github.rest.issues.addAssignees({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber, | ||
| assignees: [prAuthor] | ||
| }); | ||
|
|
||
| console.log(`Assigned issue #${issueNumber} to ${prAuthor}`); | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber, | ||
| body: `This issue has been automatically assigned to @${prAuthor} who opened PR #${prNumber} to address it. 🎯` | ||
| }); | ||
|
|
||
| } catch (error) { | ||
| if (error.status === 404) { | ||
| console.log(`Issue #${issueNumber} not found`); | ||
| } else { | ||
| console.error(`Error processing issue #${issueNumber}:`, error.message); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||||||||||||
| name: PR Reopen Reassignment | ||||||||||||||||
|
|
||||||||||||||||
| on: | ||||||||||||||||
| pull_request_target: | ||||||||||||||||
| types: [reopened] | ||||||||||||||||
| issue_comment: | ||||||||||||||||
| types: [created] | ||||||||||||||||
|
|
||||||||||||||||
| permissions: | ||||||||||||||||
| issues: write | ||||||||||||||||
| pull-requests: read | ||||||||||||||||
|
|
||||||||||||||||
| jobs: | ||||||||||||||||
| reassign-on-reopen: | ||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||
| if: github.event_name == 'pull_request_target' && github.event.action == 'reopened' | ||||||||||||||||
| steps: | ||||||||||||||||
| - name: Reassign issues on PR reopen | ||||||||||||||||
| uses: actions/github-script@v7 | ||||||||||||||||
| with: | ||||||||||||||||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||
| script: | | ||||||||||||||||
| const prBody = context.payload.pull_request.body || ''; | ||||||||||||||||
| const prAuthor = context.payload.pull_request.user.login; | ||||||||||||||||
| const prNumber = context.payload.pull_request.number; | ||||||||||||||||
| const issuePattern = /(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi; | ||||||||||||||||
| const matches = [...prBody.matchAll(issuePattern)]; | ||||||||||||||||
| if (matches.length === 0) { | ||||||||||||||||
| console.log('No linked issues found in PR body'); | ||||||||||||||||
| return; | ||||||||||||||||
| } | ||||||||||||||||
| const processedIssues = new Set(); | ||||||||||||||||
| for (const match of matches) { | ||||||||||||||||
| const issueNumber = parseInt(match[1], 10); | ||||||||||||||||
| if (processedIssues.has(issueNumber)) { | ||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
| processedIssues.add(issueNumber); | ||||||||||||||||
| try { | ||||||||||||||||
| const issue = await github.rest.issues.get({ | ||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||
| repo: context.repo.repo, | ||||||||||||||||
| issue_number: issueNumber | ||||||||||||||||
| }); | ||||||||||||||||
| if (issue.data.pull_request) { | ||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
| const currentAssignees = issue.data.assignees?.map(a => a.login) || []; | ||||||||||||||||
| if (currentAssignees.length > 0 && !currentAssignees.includes(prAuthor)) { | ||||||||||||||||
| console.log(`Issue #${issueNumber} is assigned to others, skipping`); | ||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
| if (!currentAssignees.includes(prAuthor)) { | ||||||||||||||||
| await github.rest.issues.addAssignees({ | ||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||
| repo: context.repo.repo, | ||||||||||||||||
| issue_number: issueNumber, | ||||||||||||||||
| assignees: [prAuthor] | ||||||||||||||||
| }); | ||||||||||||||||
| console.log(`Reassigned issue #${issueNumber} to ${prAuthor}`); | ||||||||||||||||
| await github.rest.issues.createComment({ | ||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||
| repo: context.repo.repo, | ||||||||||||||||
| issue_number: issueNumber, | ||||||||||||||||
| body: `Welcome back, @${prAuthor}! 🎉 This issue has been reassigned to you as you've reopened PR #${prNumber}.` | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| console.error(`Error processing issue #${issueNumber}:`, error.message); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| try { | ||||||||||||||||
| await github.rest.issues.removeLabel({ | ||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||
| repo: context.repo.repo, | ||||||||||||||||
| issue_number: prNumber, | ||||||||||||||||
| name: 'stale' | ||||||||||||||||
| }); | ||||||||||||||||
| } catch (e) { | ||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| const status = e?.status ?? e?.response?.status; | |
| if (status === 404) { | |
| console.log(`'stale' label not found on PR #${prNumber}, nothing to remove.`); | |
| } else { | |
| console.error(`Failed to remove 'stale' label from PR #${prNumber}:`, e); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
GitHub Actions expression language toLower function documentation💡 Result:
GitHub Actions’ expression language does not have a
toLower()/toLowerCase()function. The official “Evaluate expressions” docs list the built-in functions (e.g.,contains,startsWith,endsWith,format,join,toJSON,fromJSON,hashFiles,case), andtoLoweris not among them. Comparisons and some functions are already case-insensitive (for example,contains,startsWith, andendsWith). [1]If you need an actual lowercase transformation, you must do it in a step (shell) or script (e.g., JavaScript via
actions/github-script, where expressions are evaluated before being passed into the script). [2]Sources:
[1] GitHub Docs — “Evaluate expressions in workflows and actions” (Functions section) (docs.github.com)
[2]
actions/github-scriptREADME (expressions evaluated beforescript) (github.com)Citations:
Remove invalid
toLower()calls from the jobifcondition.GitHub Actions expressions don't support
toLower(), but the built-incontains()function is already case-insensitive. Simply remove thetoLower()wrapping:Suggested fix
🧰 Tools
🪛 actionlint (1.7.10)
[error] 15-15: undefined function "toLower". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson"
(expression)
[error] 16-16: undefined function "toLower". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson"
(expression)
[error] 17-17: undefined function "toLower". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson"
(expression)
[error] 18-18: undefined function "toLower". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson"
(expression)
[error] 19-19: undefined function "toLower". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson"
(expression)
🤖 Prompt for AI Agents