Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 57 additions & 0 deletions .github/workflows/issue-assignment.yml
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
});
Comment on lines +13 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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), and toLower is not among them. Comparisons and some functions are already case-insensitive (for example, contains, startsWith, and endsWith). [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-script README (expressions evaluated before script) (github.com)

Citations:


Remove invalid toLower() calls from the job if condition.

GitHub Actions expressions don't support toLower(), but the built-in contains() function is already case-insensitive. Simply remove the toLower() wrapping:

Suggested fix
-    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')
+    if: |
+      github.event.issue.pull_request == null &&
+      (
+        contains(github.event.comment.body, 'assign this issue to me') ||
+        contains(github.event.comment.body, 'assign me') ||
+        contains(github.event.comment.body, 'can i work on this') ||
+        contains(github.event.comment.body, 'i would like to work on this') ||
+        contains(github.event.comment.body, 'i want to work on this')
       )
🧰 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
In @.github/workflows/issue-assignment.yml around lines 13 - 57, The job 'if'
condition uses unsupported toLower() calls around github.event.comment.body;
update the conditional to remove the toLower() wrappers and call contains()
directly on github.event.comment.body (e.g., replace
contains(toLower(github.event.comment.body), 'assign me') with
contains(github.event.comment.body, 'assign me')) so the expressions use the
built-in case-insensitive contains() correctly.

86 changes: 86 additions & 0 deletions .github/workflows/pr-issue-link.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: PR Issue Auto-Assignment
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description ends with “Fixes #541”, but #541 appears to be an unrelated (already-merged) UI PR in this repository’s history. If the intent is to close an issue for this automation feature, the reference likely needs to point to the correct issue number (or be removed to avoid auto-closing the wrong item).

Copilot uses AI. Check for mistakes.

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);
}
}
}
154 changes: 154 additions & 0 deletions .github/workflows/pr-reopen-reassign.yml
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) {

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty catch block swallows failures when removing the stale label, which makes debugging harder (e.g., permission changes, API errors). Consider at least logging the error or explicitly handling the expected 404 case.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
}

handle-contributor-activity:
runs-on: ubuntu-latest
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null
steps:
- name: Check and handle stale PR activity
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.issue.number;
const commenter = context.payload.comment.user.login;
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
if (commenter !== pr.user.login) {
console.log('Comment not from PR author, skipping');
return;
}
const labels = context.payload.issue.labels?.map(l => l.name) || [];
if (!labels.includes('stale')) {
console.log('PR is not stale, skipping');
return;
}
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: 'stale'
});
console.log('Removed stale label');
} catch (e) {
console.log('Could not remove stale label:', e.message);
}
const prBody = pr.body || '';
const issuePattern = /(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi;
const matches = [...prBody.matchAll(issuePattern)];
for (const match of matches) {
const issueNumber = parseInt(match[1], 10);
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) {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
assignees: [commenter]
});
console.log(`Reassigned issue #${issueNumber} to ${commenter}`);
}
} catch (error) {
console.error(`Error reassigning issue #${issueNumber}:`, error.message);
}
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `Thanks for following up, @${commenter}! 🙌 The stale status has been removed and the linked issue(s) have been reassigned to you. Looking forward to your updates!`
});
Loading
Loading