Skip to content

Conversation

@Ritesh-251
Copy link

@Ritesh-251 Ritesh-251 commented Jan 14, 2026

  • Automatically unassigns stale issues after grace period
  • Checks for open PRs authored by assignee
  • Supports minutes (testing) and days (production)
  • Tested with 5-minute grace period in talawa-admin
  • Includes retry logic and dry-run capability

Resolves #24

Summary by CodeRabbit

  • Chores
    • Added an automated stale-issue unassignment workflow that runs daily or can be triggered manually. Issues labeled "stale" past a configurable grace period can be auto-unassigned (supports dry-run), optionally receive templated comments, and are processed up to a per-run limit. The job skips archived repos, missing labels, PRs, or issues with open linked PRs, and emits a run summary with counts and errors.

✏️ Tip: You can customize this high-level summary in your review settings.

- Automatically unassigns stale issues after grace period
- Checks for open PRs authored by assignee
- Supports minutes (testing) and days (production)
- Tested with 5-minute grace period in talawa-admin
- Includes retry logic and dry-run capability

Resolves PalisadoesFoundation#24
@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

Walkthrough

Adds a new composite GitHub Action and a scheduled workflow that automatically unassigns issues labeled as stale after a configurable grace period, checks linked open PRs and assignees, handles API retries with backoff, and posts templated comments; files added at .github/actions/stale-unassign/action.yml and .github/workflows/stale-unassign.yml.

Changes

Cohort / File(s) Summary
Action metadata & logic
\.github/actions/stale-unassign/action.yml
New composite action exposing inputs (grace-period minutes/days, dry-run, stale-label, exclusion labels, comment template, max-issues-per-run, token). Implements input validation, exponential backoff retry helper, repo archived check, stale-label existence and exclusion label validation, listing capped open issues with label, per-issue timeline checks for linked PRs, grace-period calculation, unassign (or dry-run) and templated comment posting, and per-run summary output. Review attention: long embedded script, retry logic, timeline event parsing, PR-author vs assignee logic, dry-run branching.
Workflow trigger
\.github/workflows/stale-unassign.yml
New workflow "Auto-unassign Stale Issues" triggered daily (midnight UTC) and via workflow_dispatch; sets minimal permissions and runs the local action with token and grace-period inputs (defaults provided). Review attention: permission scopes and input passthrough.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as GitHub Scheduler
    participant Workflow as stale-unassign.yml
    participant Action as stale-unassign Action
    participant API as GitHub API
    participant Repo as Repository

    Scheduler->>Workflow: Trigger (daily or manual)
    Workflow->>Action: Start with inputs (grace period, dry-run, etc.)
    Action->>API: Check repository archived status
    API-->>Action: Repo status

    Action->>API: Verify stale label & exclusion labels
    API-->>Action: Label existence

    Action->>API: List open issues with stale label (capped)
    API-->>Action: Issues list

    loop For each issue
        Action->>API: Get issue details & timeline events
        API-->>Action: Details & events
        Action->>Action: Detect open linked PRs and authors
        alt Blocking open linked PR by assignee exists
            Action->>Action: Skip issue
        else No blocking PR
            Action->>Action: Compute time since stale label added
            alt Grace period exceeded
                Action->>API: Remove assignees (or log dry-run)
                API-->>Action: Confirmation
                Action->>API: Post comment using template (or log dry-run)
                API-->>Action: Comment created
            else Grace period not exceeded
                Action->>Action: Skip issue
            end
        end
    end

    Action-->>Workflow: Emit summary (processed/skipped/errors)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble at stale labels in the night,
I hop, unassign, and set things right,
A templated note for those set free,
Grace counted, PRs checked carefully,
Hooray — tidy issues, tidy me!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a GitHub Action for automatic stale issue unassignment.
Linked Issues check ✅ Passed The implementation meets all requirements from issue #24: checks archived status, identifies stale issues, validates assignees, checks for open PRs by assignee, implements grace period logic, unassigns after expiry, and posts comments.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the stale unassign feature: the action metadata file and workflow configuration have no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Our Pull Request Approval Process

This PR will be reviewed according to our:

  1. Palisadoes Contributing Guidelines

  2. AI Usage Policy

Your PR may be automatically closed if:

  1. Our PR template isn't filled in correctly
  2. You haven't correctly linked your PR to an issue

👉 https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue

Thanks for contributing!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.github/actions/stale-unassign/action.yml:
- Around line 69-75: The Retry-After handling block currently assumes a numeric
value and uses parseInt which yields NaN for HTTP-date strings; update the logic
inside the same conditional (the block that reads retryAfter from
error.response.headers['retry-after']) to detect whether retryAfter is numeric
and, if not, parse it as an HTTP-date (using Date.parse) to compute
milliseconds; ensure you only call Math.max with valid numbers (fallback to
existing waitTime if parsing fails) so setTimeout never receives NaN.

In @.github/workflows/stale-unassign.yml:
- Around line 49-52: Update the example comment-template to remove the extra '@'
in the placeholder so it doesn't produce a duplicate at-sign; replace the
current placeholder syntax @{assignee} with {assignee} in the comment-message
example (referencing the comment-message key in the workflow) so the action,
which adds '@' itself, will render a single `@username`.
🧹 Nitpick comments (2)
.github/actions/stale-unassign/action.yml (2)

121-125: Consider throwing an error for invalid stale-label instead of silent return.

When stale-label is empty, the script returns silently and the workflow succeeds. This could mask configuration issues. Throwing an error would make misconfiguration more visible.

Suggested fix
          // Validate stale-label is non-empty
          if (!staleLabel || staleLabel.trim() === '') {
-           console.error('Error: stale-label input is empty. Cannot proceed.');
-           return;
+           throw new Error('stale-label input is empty. Cannot proceed.');
          }

323-326: Consider tracking comment failures in error metrics.

Comment errors are logged but not reflected in errorCount, while assignee removal errors (lines 301-306) do increment it. If comment failures become frequent, they'd be invisible in the summary report.

Optional: Track comment errors
+             let commentErrorCount = 0;
+             // ... (at top of script)
+
              } catch (error) {
                console.error(`Issue #${issue.number}: Error posting comment: ${error.message}`);
-               // Don't fail the whole workflow for a comment error
+               commentErrorCount++;
              }

Then include in the summary table.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1e0091 and b382376.

📒 Files selected for processing (2)
  • .github/actions/stale-unassign/action.yml
  • .github/workflows/stale-unassign.yml
🔇 Additional comments (6)
.github/actions/stale-unassign/action.yml (4)

1-34: LGTM - Well-designed action inputs.

Good security practice reading inputs via environment variables to prevent script injection. The grace-period-minutes for testing with override behavior is a thoughtful addition for development workflows.


222-249: LGTM - Solid linked PR detection logic.

Correctly checks all required criteria: same repository, PR is open (ignoring closed/merged), and authored by one of the assignees. The logging for non-matching PRs aids debugging.


260-283: LGTM - Grace period calculation handles re-labeling correctly.

Using the most recent label event handles edge cases where the stale label was removed and re-added, resetting the grace period appropriately.


338-349: LGTM - Good use of job summary for visibility.

The summary table provides clear metrics for each run, making it easy to monitor the action's effectiveness over time.

.github/workflows/stale-unassign.yml (2)

30-33: LGTM - Appropriately scoped permissions.

Minimal permissions following principle of least privilege: issues: write for unassignment and commenting, contents: read for checkout, pull-requests: read for linked PR detection.


40-48: LGTM - Clean action invocation.

The checkout step correctly precedes the local action reference, and input fallbacks (|| '0', || '7') properly handle scheduled runs where github.event.inputs is null.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

- Enhanced Retry-After header parsing to support both numeric (seconds) and HTTP-date formats
- Added validation to prevent NaN from being passed to setTimeout
- Fixed duplicate '@' in comment-message example by removing '@' from {assignee} placeholder
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 14, 2026
@Ritesh-251
Copy link
Author

@palisadoes pls review i have tested it fully

Copy link
Contributor

@palisadoes palisadoes left a comment

Choose a reason for hiding this comment

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

Please explain how this works.

  1. There are lots of inputs
  2. It's not clear whether the grace period is after the label is found, though I assume it is.

@Ritesh-251
Copy link
Author

Ritesh-251 commented Jan 18, 2026

@palisadoes
Addressing your concerns :
1.the input part i.e why so many inputs
Inputs like minutes and days exist to make it more optimal for testing . I could have just kept one input here i.e second but for readability purposes I kept them two later on minutes are also converted to days as you can see
Now inputs like dry run and max issue unassigne :- exist for safety purposes I was learning yml side by side for this i prefered the most safe ways for it..
2. Grace period i.e when does it start ritesh
So it starts as you assumed after only when the label stale issue is added
Piece of code doing this


const labelEvents = timelineEvents.filter(event =>
  event.event === 'labeled' &&
  event.label?.name === staleLabel
);

if (labelEvents.length === 0) {
  return;
}
labelEvents.sort(
  (a, b) => new Date(a.created_at) - new Date(b.created_at)
);

const latestLabelEvent = labelEvents[labelEvents.length - 1];
const labelAddedDate = new Date(latestLabelEvent.created_at);
const now = new Date();
const daysSinceLabel =
  (now - labelAddedDate) / (1000 * 60 * 60 * 24);

@Ritesh-251
Copy link
Author

@palisadoes pls review and tell me if any more change is required

@palisadoes
Copy link
Contributor

Please update the script so that the Action isn't activated for any one of a list of labels not just the no-issue-activity label.

- Renamed 'stale-label' input to 'exclusion-labels' to accept comma-separated list
- Updated logic to process issues with ANY of the specified exclusion labels
- Added validation to check each label exists in repository
- Maintained backward compatibility with default 'no-issue-activity' label
- Added example in workflow file showing multi-label configuration
@Ritesh-251
Copy link
Author

@palisadoes done

@palisadoes
Copy link
Contributor

@coderabbitai full review based on your configuration. Provide your output in a readable format.

This GitHub action aims to unassign people from issues if a specific label is assigned to the issue and there are no PRs referencing the issue after a timeout period. There is the option of the action exiting without implementing the feature if one or more specified labels are applied.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the action definition file
cat .github/actions/stale-unassign/action.yml

Length of output: 17005


🏁 Script executed:

#!/bin/bash
# Read the workflow file
cat .github/workflows/stale-unassign.yml

Length of output: 1873


@palisadoes, I'll provide a comprehensive review of the stale-unassign GitHub Action based on the implementation. Here's my analysis:

📋 Overall Assessment

The action is well-structured with robust error handling and retry logic. However, there's a critical naming and logic inconsistency that makes the configuration confusing and potentially contradicts the stated objectives.


🔴 Critical Issues

1. Naming Confusion: "exclusion-labels" vs Actual Behavior

Problem: The input is named exclusion-labels, but the code actually uses these labels as trigger labels (labels that activate the unassignment), not exclusion labels (labels that prevent action).

Current behavior:

// This filters issues that HAVE the exclusion labels
const issues = allIssues.filter(issue => {
  const issueLabels = issue.labels.map(l => l.name);
  return validExclusionLabels.some(label => issueLabels.includes(label));
});

What's happening: Issues WITH no-issue-activity (or other "exclusion" labels) are being processed, not excluded.

Expected interpretation based on naming:

  • Trigger label (e.g., no-issue-activity): Label that starts the grace period countdown
  • Exclusion labels (e.g., waiting-for-maintainer, blocked): Labels that prevent unassignment even if trigger label is present

Your comment mentions: "There is the option of the action exiting without implementing the feature if one or more specified labels are applied." This suggests there should be BOTH:

  1. A trigger label (starts the countdown)
  2. Exclusion labels (causes exit/skip)

But the current implementation only has one set of labels doing both jobs, which is confusing.

Recommendation:

inputs:
  stale-label:
    description: 'Label that triggers unassignment countdown (e.g., "no-issue-activity")'
    required: false
    default: 'no-issue-activity'
  exclusion-labels:
    description: 'Comma-separated labels that prevent unassignment (e.g., "waiting-for-maintainer,blocked")'
    required: false
    default: ''

🟡 Design Questions

2. PR Link Logic Clarification

The code checks for open PRs authored by the assignee:

const isAuthoredByAssignee = assigneeLogins.includes(prAuthor);
return isSameRepo && isOpen && isAuthoredByAssignee;

Current behavior: Skips unassignment if assignee has an open linked PR (they're working).

Question: Should this also check for ANY open PR linked to the issue (not just by assignee)? If someone else has an active PR for the issue, should we still unassign the original assignee?

From PR objectives: "Authored by the issue assignee" suggests current logic is correct, but please confirm this is the desired behavior.


3. Multiple Assignees Handling

When an issue has multiple assignees:

// Removes ALL assignees at once
await github.rest.issues.removeAssignees({
  assignees: assigneeLogins
});

Behavior: All assignees are removed together, even if only one is inactive.

Question: Is this intended? Should the action evaluate each assignee individually (checking if each has their own PR)?


🟢 Strengths

4. Excellent Error Handling

  • ✅ Retry logic with exponential backoff
  • ✅ Proper handling of Retry-After header (both numeric seconds and HTTP-date formats)
  • ✅ Rate limiting protection with configurable max-issues-per-run
  • ✅ Per-issue error handling that doesn't fail entire workflow

5. Security Best Practices

  • ✅ Uses environment variables to prevent script injection
  • ✅ Validates and sanitizes all inputs
  • ✅ Checks for archived repositories

6. Testing & Safety Features

  • ✅ Dry-run mode for safe testing
  • ✅ Minutes override for quick testing
  • ✅ Comprehensive logging
  • ✅ Job summary output

7. Robust Validation

  • ✅ Verifies labels exist before processing
  • ✅ Warns if comment template lacks {assignee} placeholder
  • ✅ Handles missing or invalid numeric inputs
  • ✅ Filters out pull requests from issues list

🔵 Minor Improvements

8. Workflow Configuration

.github/workflows/stale-unassign.yml:

  • ✅ Proper permissions (issues: write, contents: read, pull-requests: read)
  • ✅ Scheduled and manual triggers
  • ⚠️ Commented-out customization options might be missed by users

Suggestion: Add a README or documentation showing common configurations.


9. Grace Period Calculation

const daysSinceLabel = (now - labelAddedDate) / (1000 * 60 * 60 * 24);
  • ✅ Correct calculation
  • ⚠️ Uses floating-point comparison which is fine, but consider if you want fractional days or whole days

10. Timeline Event Lookup

const labelEvents = timelineEvents.filter(event =>
  event.event === 'labeled' &&
  validExclusionLabels.includes(event.label?.name)
);
  • ✅ Finds most recent label event
  • ⚠️ Could be slow for issues with very long timelines, but pagination is used so should be acceptable

📝 Documentation Gaps

11. Missing Documentation

The action would benefit from:

  • Clear explanation of the label workflow (trigger vs exclusion)
  • Examples of common use cases
  • Explanation of the PR linking behavior
  • Migration guide if labels are renamed

🎯 Summary & Recommendations

Category Status Priority
Naming consistency (exclusion-labels) ❌ Needs fix HIGH
Logic clarity (trigger vs exclusion) ⚠️ Needs clarification HIGH
Multiple assignees behavior ⚠️ Needs confirmation MEDIUM
Error handling ✅ Excellent -
Security ✅ Good practices -
Testing support ✅ Well implemented -

Key Action Items:

  1. Rename or restructure the label system to clearly separate trigger labels from exclusion labels
  2. Clarify in documentation whether the current single-label approach is intended
  3. Confirm the multiple assignees behavior is correct for your use case
  4. Add a README explaining configuration options and examples

The action is functionally solid with excellent error handling, but the naming confusion around "exclusion-labels" could lead to misconfiguration. Once this is clarified or fixed, it will be production-ready.

Would you like me to help create a refactored version that separates trigger and exclusion labels, or is the current single-label behavior actually what you intended?

✅ Actions performed

Full review triggered.

@palisadoes
Copy link
Contributor

@coderabbitai

In reference to this analysis:

These are the responses:

  • PR Link Logic Clarification This is the desired outcome
  • Multiple Assignees Handling: This is the desired outcome

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Tip

For best results, initiate chat on the files or code changes.

@palisadoes, thank you for confirming those design decisions! I've noted them for the review.

That leaves the critical naming inconsistency as the remaining issue to resolve. The current implementation has a logical contradiction:

🔴 Remaining Critical Issue: "exclusion-labels" Logic

Current code behavior:

// Line ~155 - Filters issues that HAVE the exclusion labels
const issues = allIssues.filter(issue => {
  const issueLabels = issue.labels.map(l => l.name);
  return validExclusionLabels.some(label => issueLabels.includes(label));
});

What this does: Issues WITH no-issue-activity are processed for unassignment (these labels TRIGGER the action).

Problem: The input is named exclusion-labels, but it's actually being used as trigger labels (labels that activate the unassignment countdown).


❓ Clarification Needed

In your earlier comment you mentioned: "There is the option of the action exiting without implementing the feature if one or more specified labels are applied."

Question: Should there be TWO separate label concepts?

Option A - Current Implementation (Single Label System):

  • no-issue-activity label → Starts countdown → After grace period → Unassigns
  • No way to prevent/exclude certain issues from being unassigned

Option B - Dual Label System:

  • Trigger label (e.g., no-issue-activity) → Starts countdown
  • Exclusion labels (e.g., waiting-for-maintainer, blocked) → Prevents unassignment even if trigger label present

Which behavior do you want?

  • If Option A is correct, I recommend renaming exclusion-labels to stale-labels or trigger-labels to match the actual behavior.
  • If Option B is what you want, the code needs to be updated to implement proper exclusion logic.

Please clarify which approach you'd like, and I can help implement or document it accordingly.

Copy link
Contributor

@palisadoes palisadoes left a comment

Choose a reason for hiding this comment

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

Please address the critical issue


image

- stale-label: triggers unassignment countdown (default: no-issue-activity)
- exclusion-labels: PREVENTS unassignment when present (e.g., waiting-for-maintainer,blocked)
- Fixed naming confusion: exclusion labels now correctly exclude/protect issues
- Updated workflow comments to clarify the dual-label system
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub Action - Automatic unassignment of stale issues

2 participants