Skip to content

Replace native browser alerts with a Toast/notification component in Login #128

Replace native browser alerts with a Toast/notification component in Login

Replace native browser alerts with a Toast/notification component in Login #128

Workflow file for this run

name: Issue Assignment Manager
on:
issues:
types: [assigned]
issue_comment:
types: [created]
schedule:
- cron: '0 */6 * * *' # every 6 hours (UTC)
permissions:
issues: write
pull-requests: write
contents: read
jobs:
manual-assignment-welcome:
if: ${{ github.event_name == 'issues' && github.event.action == 'assigned' && !github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Welcome manually assigned contributor
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const core = require("@actions/core");
const { context, github } = require("@actions/github");
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue = context.payload.issue;
const assignee = context.payload.assignee;
const assigner = context.payload.assigner || context.payload.sender;
if (!issue || !assignee) {
core.info("Missing issue or assignee. Skipping.");
return;
}
const issueNumber = issue.number;
const marker = `<!-- learnhub-manual-assign:${assignee.login.toLowerCase()} -->`;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100
});
const alreadyCommented = comments.some(comment =>
String(comment.body || "").includes(marker)
);
if (alreadyCommented) {
core.info(`Manual assignment welcome already exists for @${assignee.login}.`);
return;
}
const intro =
`👋 Hi @${assignee.login}, you have been assigned to this issue by @${assigner?.login || "a maintainer"}.\n\n`;
const checklist =
`Welcome to LearnHub. Please make sure you:\n` +
`- Star the repository\n` +
`- Fork the repository\n` +
`- Read \`CONTRIBUTING.md\`\n` +
`- Read \`CODE_OF_CONDUCT.md\`\n` +
`- Understand the issue clearly before starting\n\n`;
const timing =
`Please share meaningful progress within 3 days, or the issue may be reassigned.\n\n`;
const support =
`If you need help or may be delayed, please ping @udaycodespace or join Discord and message there with your GitHub username.\n\n`;
const note =
`Note: PRs with fewer than 20 lines are not accepted.\n`;
const body = `${marker}\n${intro}${checklist}${timing}${support}${note}`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body
});
handle-commands:
if: ${{ github.event_name == 'issue_comment' && !github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Handle /assign, /work, /unassign commands
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const core = require("@actions/core");
const { context, github } = require("@actions/github");
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue = context.payload.issue;
const comment = context.payload.comment;
const issueNumber = issue.number;
const issueState = String(issue.state || "").toLowerCase();
const commentBody = String(comment.body || "").trim();
const commenter = comment.user.login;
const issueAuthor = issue.user.login;
const isAssignCommand = /^\/assign(?:\s+(.+))?$/i.test(commentBody);
const assignMatch = commentBody.match(/^\/assign(?:\s+(.+))?$/i);
const isWorkCommand = /^\/work\s+(.+)$/i.test(commentBody);
const workMatch = commentBody.match(/^\/work\s+(.+)$/i);
const isUnassignCommand = /^\/unassign\s*$/i.test(commentBody);
if (!isAssignCommand && !isWorkCommand && !isUnassignCommand) {
return;
}
if (issueState === "closed") {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, this issue is already closed and cannot be assigned, worked, or unassigned.`
});
return;
}
async function isMaintainer(username) {
try {
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username
});
return (
["admin", "write"].includes(permission.permission) ||
["admin", "maintain", "write"].includes(permission.role_name)
);
} catch (e) {
return false;
}
}
const currentAssignees = (issue.assignees || []).map(a => a.login);
const lowerCurrentAssignees = currentAssignees.map(login => login.toLowerCase());
const commenterLower = commenter.toLowerCase();
const issueAuthorLower = issueAuthor.toLowerCase();
async function hasTooManyOpenAssignments(username) {
const { data: assignedItems } = await github.rest.issues.listForRepo({
owner,
repo,
assignee: username,
state: "open",
per_page: 100
});
const assignedOpenIssuesOnly = assignedItems.filter(item => !item.pull_request);
return assignedOpenIssuesOnly.length >= 2;
}
const limitBody =
`Easy there, legend — @${commenter} already has 2 open issues. Finish one first and let others cook too.\n`;
async function replyLimit(username) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: limitBody.replace(commenter, username)
});
}
const QUEUE_MARKER = "<!-- learnhub-queue-state";
async function getQueueComment() {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100
});
return comments.find(c => String(c.body || "").includes(QUEUE_MARKER)) || null;
}
function parseQueueState(body) {
const state = {
primary: "",
primary_assigned_at: "",
backup: "",
backup_approach: "",
last_progress_at: ""
};
if (!body) return state;
const lines = String(body).split("\n");
for (const line of lines) {
const m = line.match(/^\s*([a-z_]+):\s*(.*)\s*$/i);
if (m) {
const key = m[1].trim();
const val = m[2].trim();
if (Object.prototype.hasOwnProperty.call(state, key)) {
state[key] = val;
}
}
}
return state;
}
function buildQueueBody(state) {
return (
`${QUEUE_MARKER}\n` +
`primary: ${state.primary || ""}\n` +
`primary_assigned_at: ${state.primary_assigned_at || ""}\n` +
`backup: ${state.backup || ""}\n` +
`backup_approach: ${state.backup_approach || ""}\n` +
`last_progress_at: ${state.last_progress_at || ""}\n` +
`-->`
);
}
async function ensureQueueComment() {
let qc = await getQueueComment();
if (!qc) {
const initialState = {
primary: "",
primary_assigned_at: "",
backup: "",
backup_approach: "",
last_progress_at: ""
};
const body = buildQueueBody(initialState);
const { data: created } = await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body
});
qc = created;
}
return qc;
}
const queueComment = await ensureQueueComment();
let queueState = parseQueueState(queueComment.body || "");
const commenterIsMaintainer = await isMaintainer(commenter);
const isSelfIssue = commenterLower === issueAuthorLower;
// /assign (self or @someone)
if (isAssignCommand) {
const rawArg = (assignMatch[1] || "").trim();
const isForceAssign = /^@([a-zA-Z0-9-]+)$/i.test(rawArg);
const forceMatch = rawArg.match(/^@([a-zA-Z0-9-]+)$/i);
const targetUser = isForceAssign ? forceMatch[1] : commenter;
if (issue.pull_request) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, assignment commands are only for issues, not pull requests.`
});
return;
}
// /assign @someone (maintainer-only)
if (isForceAssign) {
if (!commenterIsMaintainer) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, only maintainers can assign other users directly.`
});
return;
}
if (await hasTooManyOpenAssignments(targetUser)) {
await replyLimit(targetUser);
return;
}
const isAlreadyPrimary = queueState.primary.toLowerCase() === targetUser.toLowerCase();
if (!isAlreadyPrimary && currentAssignees.length > 0) {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: currentAssignees
});
}
if (!lowerCurrentAssignees.includes(targetUser.toLowerCase())) {
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: [targetUser]
});
}
const now = new Date().toISOString();
queueState.primary = targetUser;
queueState.primary_assigned_at = now;
queueState.backup = "";
queueState.backup_approach = "";
queueState.last_progress_at = now;
await github.rest.issues.updateComment({
owner,
repo,
comment_id: queueComment.id,
body: buildQueueBody(queueState)
});
const labels = (issue.labels || []).map(l => String(l.name || "").toLowerCase());
const labelsToAdd = ["assigned"].filter(label => !labels.includes(label));
if (labelsToAdd.length > 0) {
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: labelsToAdd
});
} catch (e) {}
}
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "available"
});
} catch (e) {}
const forceBody =
`🎯 @${targetUser} has been assigned to this issue by a maintainer. ` +
`Please share meaningful progress within 3 days, or it may be opened to others.\n`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: forceBody
});
return;
}
// Plain /assign (self-claim)
if (await hasTooManyOpenAssignments(commenter)) {
await replyLimit(commenter);
return;
}
const hasPrimaryAssignee = currentAssignees.length > 0;
if (!hasPrimaryAssignee) {
if (!lowerCurrentAssignees.includes(commenterLower)) {
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: [commenter]
});
}
const now = new Date().toISOString();
queueState.primary = commenter;
queueState.primary_assigned_at = now;
queueState.backup = "";
queueState.backup_approach = "";
queueState.last_progress_at = now;
await github.rest.issues.updateComment({
owner,
repo,
comment_id: queueComment.id,
body: buildQueueBody(queueState)
});
const labels = (issue.labels || []).map(l => String(l.name || "").toLowerCase());
const labelsToAdd = ["assigned"].filter(label => !labels.includes(label));
if (labelsToAdd.length > 0) {
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: labelsToAdd
});
} catch (e) {}
}
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "available"
});
} catch (e) {}
const authorWelcome =
`🎉 @${commenter} has been assigned to this issue. Welcome to LearnHub.\n\n` +
`Hope you do well on this issue. Please make sure you:\n` +
`- Star the repository\n` +
`- Fork the repository\n` +
`- Read \`CONTRIBUTING.md\`\n` +
`- Read \`CODE_OF_CONDUCT.md\`\n` +
`- Understand the issue clearly before starting\n` +
`- Share meaningful progress within 3 days\n\n` +
`If you need help or may be delayed, please ping @udaycodespace.\n\n` +
`Note: PRs with fewer than 20 lines are not accepted.\n`;
const nonAuthorWelcome =
`🎉 @${commenter} has been assigned to this issue. Welcome to LearnHub.\n\n` +
`Please make sure you:\n` +
`- Star the repository\n` +
`- Fork the repository\n` +
`- Read \`CONTRIBUTING.md\`\n` +
`- Read \`CODE_OF_CONDUCT.md\`\n` +
`- Understand the issue clearly before starting\n\n` +
`Please share meaningful progress within 3 days, or it may be made available to others.\n`;
const welcomeBody = isSelfIssue ? authorWelcome : nonAuthorWelcome;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: welcomeBody
});
} else {
if (lowerCurrentAssignees.includes(commenterLower)) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, you are already assigned to this issue. Please focus on making progress and sharing updates within 3 days.`
});
} else {
const primary = currentAssignees[0];
const alreadyBody =
`@${commenter}, first priority for this issue is with @${primary}.\n\n` +
`If you’d like to be next in queue, please use \`/work <approach>\` and describe how you plan to solve it. ` +
`After the 3-day window, you may be auto-promoted if there is no meaningful progress from the primary assignee.\n`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: alreadyBody
});
}
}
return;
}
// /work <approach>
if (isWorkCommand) {
if (issue.pull_request) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, work queue commands are only for issues, not pull requests.`
});
return;
}
const approach = workMatch[1].trim();
if (!approach) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, please provide your approach after \`/work\`. Example: \`/work I will reproduce the issue locally, trace the flow, and submit a tested fix.\``
});
return;
}
if (await hasTooManyOpenAssignments(commenter)) {
await replyLimit(commenter);
return;
}
const primary = queueState.primary || (currentAssignees[0] || "");
if (!primary) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, this issue is currently unassigned. You can claim it directly with \`/assign\`.`
});
return;
}
if (!queueState.backup) {
queueState.backup = commenter;
queueState.backup_approach = approach;
await github.rest.issues.updateComment({
owner,
repo,
comment_id: queueComment.id,
body: buildQueueBody(queueState)
});
const workBody =
`@${commenter}, first priority is with @${primary}. You are now next in the queue for this issue with the following approach:\n\n` +
`\`${approach}\`\n\n` +
`Please make sure you:\n` +
`- Star the repository\n` +
`- Fork the repository\n` +
`- Understand the issue clearly\n\n` +
`If @${primary} doesn’t show meaningful progress within 3 days, you may be auto-promoted to primary assignee.\n`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: workBody
});
} else if (queueState.backup.toLowerCase() === commenterLower) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, you are already in the backup queue for this issue. Please focus on your described approach and wait for the 3-day window.`
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, the backup slot for this issue is already taken by @${queueState.backup}. Please pick another issue to work on.`
});
}
return;
}
// /unassign
if (isUnassignCommand) {
const isPrimary = lowerCurrentAssignees.includes(commenterLower);
if (!isPrimary && !commenterIsMaintainer) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, you cannot unassign this issue because it is not assigned to you.`
});
return;
}
const usersToRemove = commenterIsMaintainer ? currentAssignees : [commenter];
if (usersToRemove.length === 0) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `@${commenter}, this issue is already unassigned.`
});
return;
}
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: usersToRemove
});
queueState.primary = "";
queueState.primary_assigned_at = "";
queueState.backup = "";
queueState.backup_approach = "";
queueState.last_progress_at = "";
await github.rest.issues.updateComment({
owner,
repo,
comment_id: queueComment.id,
body: buildQueueBody(queueState)
});
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "assigned"
});
} catch (e) {}
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["available"]
});
} catch (e) {}
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `🧹 This issue is now unassigned and open for other contributors.`
});
return;
}
auto-promotion:
if: ${{ github.event_name == 'schedule' }}
runs-on: ubuntu-latest
steps:
- name: Auto-promote backup claimants after 3 days
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const core = require("@actions/core");
const { github, context } = require("@actions/github");
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "open",
labels: "assigned",
per_page: 100
});
const QUEUE_MARKER = "<!-- learnhub-queue-state";
function parseQueueState(body) {
const state = {
primary: "",
primary_assigned_at: "",
backup: "",
backup_approach: "",
last_progress_at: ""
};
if (!body) return state;
const lines = String(body).split("\n");
for (const line of lines) {
const m = line.match(/^\s*([a-z_]+):\s*(.*)\s*$/i);
if (m) {
const key = m[1].trim();
const val = m[2].trim();
if (Object.prototype.hasOwnProperty.call(state, key)) {
state[key] = val;
}
}
}
return state;
}
function buildQueueBody(state) {
return (
`${QUEUE_MARKER}\n` +
`primary: ${state.primary || ""}\n` +
`primary_assigned_at: ${state.primary_assigned_at || ""}\n` +
`backup: ${state.backup || ""}\n` +
`backup_approach: ${state.backup_approach || ""}\n` +
`last_progress_at: ${state.last_progress_at || ""}\n` +
`-->`
);
}
const now = Date.now();
const threeDaysMs = 3 * 24 * 60 * 60 * 1000;
for (const issue of issues) {
if (issue.pull_request) continue;
const issueNumber = issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100
});
const queueComment = comments.find(c =>
String(c.body || "").includes(QUEUE_MARKER)
);
if (!queueComment) {
continue;
}
let queueState = parseQueueState(queueComment.body || "");
const primary = queueState.primary;
const backup = queueState.backup;
if (!primary || !backup) {
continue;
}
const activityIso = queueState.last_progress_at || queueState.primary_assigned_at;
if (!activityIso) {
continue;
}
const activityTime = Date.parse(activityIso);
if (Number.isNaN(activityTime)) {
continue;
}
const inactiveForMs = now - activityTime;
if (inactiveForMs < threeDaysMs) {
continue;
}
const currentAssignees = (issue.assignees || []).map(a => a.login);
if (currentAssignees.length > 0) {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: currentAssignees
});
}
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: [backup]
});
const promotionTimeIso = new Date().toISOString();
queueState.primary = backup;
queueState.primary_assigned_at = promotionTimeIso;
queueState.backup = "";
queueState.backup_approach = "";
queueState.last_progress_at = promotionTimeIso;
await github.rest.issues.updateComment({
owner,
repo,
comment_id: queueComment.id,
body: buildQueueBody(queueState)
});
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["auto-promoted"]
});
} catch (e) {}
const promotionBody =
`⏳ The primary claimant did not show enough progress in 3 days. @${backup} is now the new priority assignee for this issue.\n\n` +
`Please:\n` +
`- Star the repository\n` +
`- Fork the repository\n` +
`- Understand the issue clearly\n` +
`- Share meaningful progress as soon as you can.\n`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: promotionBody
});
}