Skip to content

Add stale unassign GitHub Action#31

Open
Ritesh-251 wants to merge 7 commits intoPalisadoesFoundation:mainfrom
Ritesh-251:feature/stale-unassign-action
Open

Add stale unassign GitHub Action#31
Ritesh-251 wants to merge 7 commits intoPalisadoesFoundation:mainfrom
Ritesh-251:feature/stale-unassign-action

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 (runs nightly or manually). Issues labeled "stale" that exceed a configurable grace period (minutes or days) can be auto-unassigned (supports dry-run), optionally receive templated comments, and are processed up to a per-run limit. Workflow skips archived repos, missing labels, PRs, issues with open linked PRs, and produces a summary of processed, skipped, and error counts.

✏️ 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 composite GitHub Action and a scheduled workflow that unassigns issues labeled stale after a configurable grace period, with checks for archived repos, exclusion labels, open linked PRs, dry-run support, retry/backoff, and templated comments. Files added: .github/actions/stale-unassign/action.yml, .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-days, grace-period-minutes, dry-run, stale-label, exclusion-labels, comment-message, max-issues-per-run, github-token). Implements validation, archived-repo check, label verification, limited issue listing, timeline parsing to detect linked PRs/authors, grace-period computation (minutes precedence), conditional unassignment/commenting, per-issue error isolation, aggregated summary, and retry/backoff with Retry-After handling.
Workflow trigger
\.github/workflows/stale-unassign.yml
New scheduled/manual workflow "Auto-unassign Stale Issues" (daily at midnight UTC and workflow_dispatch). Grants minimal permissions and runs the local action at ./.github/actions/stale-unassign with token and grace-period inputs (defaults: 0 minutes, 7 days).

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as GitHub Scheduler
    participant Workflow as .github/workflows/stale-unassign.yml
    participant Action as .github/actions/stale-unassign
    participant API as GitHub API
    participant Repo as Repository

    Scheduler->>Workflow: Trigger (daily / manual)
    Workflow->>Action: Start with inputs (grace period, dry-run, etc.)
    Action->>API: Get repository info (archived?)
    API-->>Action: Repo status
    Action->>API: Ensure stale & exclusion labels exist
    API-->>Action: Label presence
    Action->>API: List open issues with stale label (max cap)
    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 templated comment (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 beneath the moon,
I hop and tally grace till unassign comes soon,
I peek for PRs and skip what’s not due,
I leave a note — dry-run or true,
Hooray for tidy issues — one small hop at a time!

🚥 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 summarizes the main change: adding a new GitHub Action for automatically unassigning stale issues.
Linked Issues check ✅ Passed All core requirements from issue #24 are met: repository archival check, stale label detection, assignee verification, linked PR filtering by assignee, configurable grace period, unassignment with commenting, exclusion label support, and central reusability setup.
Out of Scope Changes check ✅ Passed Both files (.github/actions/stale-unassign/action.yml and .github/workflows/stale-unassign.yml) directly support the GitHub Action implementation objective with no extraneous changes detected.
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.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
@palisadoes
Copy link
Contributor

@coderabbitai full review and reevaluate all your previous comments that may have been dismissed, ignored or resolved. Post your conclusion as an easily readable comment.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

🧩 Analysis chain

🏁 Script executed:

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

Length of output: 18508


🏁 Script executed:

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

Length of output: 1983


🏁 Script executed:

#!/bin/bash
# Check commit history to see what changes were made
git log --oneline --all -20

Length of output: 1089


@palisadoes, I've completed a full re-evaluation of the stale-unassign Action. Here's my comprehensive conclusion:


CRITICAL ISSUE RESOLVED

The naming inconsistency has been completely fixed in commit 38a5297. The Action now implements a proper dual-label system:

  • stale-label (default: no-issue-activity) → Triggers the grace period countdown
  • exclusion-labels (optional) → Prevents unassignment even when stale-label is present

The code logic now perfectly matches the input names and descriptions. Issues are fetched by the stale-label, then filtered out if they have any exclusion labels. This is exactly what was needed. ✨


🎯 DESIGN DECISIONS CONFIRMED

Both design questions from the previous review are resolved as intended:

PR-Link Logic: Correctly skips unassignment only if the assignee has authored an open PR
Multiple Assignees: Correctly removes all assignees at once (efficient batch operation)


🏆 STRENGTHS - PRODUCTION READY

1. Robust Error Handling

  • ✅ Exponential backoff retry logic with 3 attempts
  • ✅ Intelligent Retry-After header parsing (both numeric seconds and HTTP-date formats)
  • ✅ Graceful degradation: per-issue errors don't fail entire workflow
  • ✅ Rate limit protection via max-issues-per-run

2. Security Best Practices

  • ✅ Environment variables prevent script injection attacks
  • ✅ Input validation and sanitization
  • ✅ Repository archived check prevents unnecessary runs
  • ✅ Label existence validation before processing

3. Testing & Safety Features

  • Dry-run mode for safe testing without side effects
  • Grace-period-minutes override for rapid testing (takes priority over days)
  • Max-issues-per-run limit prevents runaway executions
  • ✅ Comprehensive logging at every decision point

4. Flexible Configuration

  • ✅ Customizable comment template with {assignee} placeholder
  • ✅ Optional exclusion labels (can be empty for simple use cases)
  • ✅ Configurable grace periods (minutes for testing, days for production)
  • ✅ Manual workflow_dispatch trigger with input overrides

5. Efficient Implementation

  • ✅ Batch removal of all assignees (single API call per issue)
  • ✅ Timeline event pagination for issues with long histories
  • ✅ Filters out PRs from issues list
  • ✅ Validates exclusion labels and warns on non-existent ones

6. Proper Workflow Permissions

  • ✅ Minimal required permissions: issues: write, contents: read, pull-requests: read
  • ✅ Scheduled daily runs (midnight UTC) plus manual trigger
  • ✅ Clear commented examples in workflow file

📋 LOGIC VERIFICATION

The Action correctly implements this flow:

graph TD
    A[Start] --> B{Repo archived?}
    B -->|Yes| END[Skip]
    B -->|No| C{Stale label exists?}
    C -->|No| END
    C -->|Yes| D[Fetch open issues with stale label]
    D --> E{For each issue}
    E --> F{Is PR?}
    F -->|Yes| SKIP[Skip to next]
    F -->|No| G{Has assignees?}
    G -->|No| SKIP
    G -->|Yes| H{Has exclusion label?}
    H -->|Yes| SKIP
    H -->|No| I{Has open PR by assignee?}
    I -->|Yes| SKIP
    I -->|No| J{Grace period exceeded?}
    J -->|No| SKIP
    J -->|Yes| K[Remove all assignees]
    K --> L[Post comment]
    L --> SKIP
    SKIP --> E
    E --> M[Generate summary report]
    M --> END
Loading

Key behaviors verified:

  1. ✅ Only processes issues with the stale-label (not exclusion labels)
  2. ✅ Skips issues that have any exclusion-label (protected from unassignment)
  3. ✅ Checks for open PRs authored by the assignee (not just any PR)
  4. ✅ Grace period starts from most recent stale-label application
  5. ✅ Handles re-labeling correctly (uses latest label event)
  6. ✅ All assignees removed and mentioned in a single comment

💡 MINOR SUGGESTIONS (Optional Enhancements)

1. Documentation

Consider adding a README at .github/actions/stale-unassign/README.md with:

  • Use case examples (e.g., "Issues stale for 14 days get unassigned unless waiting-for-maintainer")
  • Common configuration patterns
  • Troubleshooting guide
  • Example of label workflow: no-issue-activity → wait 7 days → unassign

2. Workflow Comments

The workflow file has helpful commented examples, but could add:

# Example use case: Unassign after 14 days, but protect issues waiting for maintainers
# exclusion-labels: 'waiting-for-maintainer,blocked,dependencies'

3. Job Summary Enhancement

Consider adding to the summary report:

  • Grace period used (days/minutes)
  • Stale label name
  • Exclusion labels (if any)
  • Dry-run status

Example:

await core.summary
  .addHeading('Stale Issue Unassignment Report')
  .addRaw(`**Configuration:** Grace period: ${gracePeriodDisplay}, Stale label: "${staleLabel}", Exclusion labels: ${exclusionDisplay}, Dry-run: ${dryRun}`)
  .addTable([...])
  .write();

4. Edge Case: Label Removed and Re-added

Current behavior: Uses the most recent label event (correct).
Consider logging when re-labeling is detected for transparency:

if (labelEvents.length > 1) {
  console.log(`Issue #${issue.number}: Stale label was re-applied ${labelEvents.length} times, using most recent`);
}

🎓 BEST PRACTICES DEMONSTRATED

This Action exemplifies excellent GitHub Actions development:

  1. Composability: Reusable action in .github/actions/ called by workflow
  2. Separation of Concerns: Action logic separate from workflow configuration
  3. Defense in Depth: Multiple layers of validation and error handling
  4. Observability: Comprehensive logging and job summaries
  5. Safety First: Dry-run mode, max limits, graceful failures
  6. User Experience: Clear input descriptions, sensible defaults, helpful warnings

🚀 FINAL VERDICT: PRODUCTION READY

Category Status Notes
Critical Issues RESOLVED Dual-label system implemented correctly
Logic Correctness VERIFIED All flows work as intended
Security STRONG Injection prevention, input validation
Error Handling ROBUST Retry logic, rate limiting, graceful degradation
Testing Support EXCELLENT Dry-run, minutes override, comprehensive logging
Documentation ⚠️ ADEQUATE Could add README (optional enhancement)
Production Readiness READY Safe to merge and deploy

📝 SUMMARY

The stale-unassign GitHub Action is well-architected, secure, and production-ready. The critical naming/logic issue has been completely resolved. The implementation demonstrates best practices in error handling, security, and testing. The only remaining suggestions are optional documentation enhancements.

Recommendation:Approve and merge. This Action is ready for use across PalisadoesFoundation repositories.

Excellent work, @Ritesh-251! 🎉

✅ Actions performed

Full review triggered.

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 56-117: The fallback DEFAULT_GRACE_PERIOD (used when
GRACE_PERIOD_DAYS is invalid) is set to 1 but the input default is 7; update the
constant or validation so they match. Change DEFAULT_GRACE_PERIOD to 7 (the
input default) or make the gracePeriodDays validation use the configured input
default value, ensuring the variables DEFAULT_GRACE_PERIOD, gracePeriodMinutes,
and gracePeriodDays in the retry/validation block are consistent so an invalid
GRACE_PERIOD_DAYS falls back to 7 days rather than 1.
- Around line 283-325: The same-repo PR detection in the hasOpenLinkedPR block
uses the undocumented event.source.issue.repository?.full_name which can be
missing; change the logic in the hasOpenLinkedPR predicate (the
timelineEvents.some callback) to parse and compare
event.source.issue.repository_url instead (extract owner/repo from the URL and
compare to `${context.repo.owner}/${context.repo.repo}`) and remove the fallback
that treats missing repository as same-repo; keep the existing checks for open
state (event.source.issue.state) and assignee author check
(assigneeLogins.includes(prAuthor)) and update the console.log messages to
reflect when repository_url mismatches or is missing.

@palisadoes
Copy link
Contributor

@Ritesh-251 Please address the findings

- Change DEFAULT_GRACE_PERIOD from 1 to 7 to match input default
- Update same-repo PR detection to use repository_url instead of undocumented full_name
- Add proper URL parsing and error handling for missing repository_url
- Remove fallback that treated missing repository as same-repo
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: 1

🤖 Fix all issues with AI agents
In @.github/actions/stale-unassign/action.yml:
- Around line 365-376: The grace-period calculation doesn’t guard against an
invalid timeline event date; validate latestLabelEvent.created_at by checking
labelAddedDate is a valid Date (e.g., isNaN(labelAddedDate.getTime())) before
computing daysSinceLabel, and if invalid log a clear message referencing Issue
#${issue.number} and latestLabelEvent, increment skippedCount and continue;
update the block that computes labelAddedDate, daysSinceLabel and the if
(daysSinceLabel < gracePeriodInDays) branch to include this defensive check so
malformed or missing created_at won’t produce NaN and cause incorrect
unassignment.
🧹 Nitpick comments (2)
.github/actions/stale-unassign/action.yml (2)

144-148: Early exits bypass job summary output.

When stale-label is empty, the action returns early without writing to core.summary. The same applies to lines 174-176 (archived repo) and 186-189 (missing label). This leaves the GitHub Actions job summary unpopulated, making it harder to diagnose why the action did nothing.

Consider writing a summary before early exits for better observability.

Example for consistent summary on early exit
          if (!staleLabel || staleLabel.trim() === '') {
            console.error('Error: stale-label input is empty. Cannot proceed.');
+           await core.summary
+             .addHeading('Stale Issue Unassignment Report')
+             .addRaw('⚠️ Action skipped: stale-label input is empty.')
+             .write();
            return;
          }

394-428: Error counts are double-counted in skipped metric.

When an error occurs (lines 396-398 and 426-427), both errorCount and skippedCount are incremented. This makes the summary misleading—if you have 10 skipped and 3 errors, 3 of those skipped are actually the errors.

Consider either excluding errors from skippedCount or clarifying the metric label.

Option 1: Don't increment skippedCount on errors
              } catch (error) {
                console.error(`Issue #${issue.number}: Error removing assignees: ${error.message}`);
                errorCount++;
-               skippedCount++;
                continue;
              }
            } catch (error) {
              console.error(`Issue #${issue.number}: Unexpected error: ${error.message}`);
              errorCount++;
-             skippedCount++;
            }
Option 2: Rename to clarify metrics include overlap
            .addTable([
              ['Metric', 'Count'],
              ['Processed', processedCount.toString()],
-             ['Skipped', skippedCount.toString()],
+             ['Skipped (incl. errors)', skippedCount.toString()],
              ['Errors', errorCount.toString()]
            ])

- Validate labelAddedDate using isNaN(labelAddedDate.getTime())
- Skip issues with malformed or missing created_at timestamps
- Prevent NaN in daysSinceLabel calculation that could cause incorrect unassignment
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: 1

🤖 Fix all issues with AI agents
In @.github/actions/stale-unassign/action.yml:
- Around line 218-240: The code currently calls github.paginate(...) causing all
pages to be fetched before applying the maxIssuesPerRun limit; update the fetch
loop to use github.paginate.iterator (or github.paginate with a done callback)
so you can break early: iterate over
github.paginate.iterator(github.rest.issues.listForRepo, { owner:
context.repo.owner, repo: context.repo.repo, state: 'open', labels: staleLabel,
per_page: 100 }) and push data into the issues array until issues.length >=
limit (where limit = maxIssuesPerRun > 0 ? maxIssuesPerRun : Infinity), then
break and slice to limit; keep the existing error handling and console.log that
references issues and staleLabel.

- Use github.paginate.iterator instead of github.paginate
- Break early when maxIssuesPerRun limit is reached
- Avoid fetching unnecessary pages to reduce API calls
- Update console.log message to reflect when limit is applied
@Ritesh-251
Copy link
Author

@palisadoes pls review i have done all the suggested changes

@palisadoes
Copy link
Contributor

@coderabbitai full review and reevaluate all your previous comments that may have been dismissed, ignored or resolved. Evaluate whether the goals of the issue linked in the first PR comment are met. If there are test cases, ensure that the updated or created tests won't be flaky, especially in a sharded testing environment where mock cleanup is important and response times are highly variable. Post your conclusion as an easily readable comment that also includes your findings outside the diffs.

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

🧩 Analysis chain

@palisadoes, I'll conduct a comprehensive review of the current state of the Action and evaluate it against all requirements.


🏁 Script executed:

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

Length of output: 20082


🏁 Script executed:

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

Length of output: 1983


🏁 Script executed:

#!/bin/bash
# Search for any test files related to this action
fd -e yml -e yaml -e js -e ts test
fd -e yml -e yaml -e js -e ts spec

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check recent commit history to see what changes were made
git log --oneline --all -30 --graph

Length of output: 1784


🏁 Script executed:

#!/bin/bash
# Search for any documentation files (README, docs, etc.)
fd -e md . .github/actions/stale-unassign

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's a root README that might document this action
cat README.md 2>/dev/null || echo "No README.md found"

Length of output: 9


@palisadoes, I've completed a comprehensive re-evaluation. Here's my detailed conclusion:


ISSUE #24 REQUIREMENTS: FULLY MET

All requirements from the linked issue are successfully implemented:

Requirement Status Implementation
Run only on non-archived repos Lines 157-167: Checks repo.archived and exits early
Identify issues with stale trigger label Lines 201-230: Fetches open issues with stale-label
Verify issue has assignee Lines 251-255: Skips if no assignees
Check for open linked PR by assignee Lines 268-318: Timeline check for same-repo, open, assignee-authored PRs
Grace period before unassignment Lines 324-356: Calculates days since label, supports minutes/days
Unassign and post comment Lines 363-399: Removes all assignees, posts customizable comment
Exclusion labels configuration Lines 11-14, 169-188, 258-265: Validates and prevents unassignment
Reusable from central repo Composite action in .github/actions/ called by workflow

🎯 PREVIOUS CODERABBIT RECOMMENDATIONS: STATUS

Implemented (Critical)

  1. Dual-label system (commit 38a5297): Correctly separated stale-label (trigger) from exclusion-labels (prevention)
  2. PR-link logic: Confirmed to check only assignee-authored PRs (lines 308-309)
  3. Multiple assignees: Confirmed to remove all at once (line 369)
  4. Optimization (commit 929b6fb): Uses paginate.iterator with early break to avoid unnecessary API calls
  5. Date validation (commit 78e44c7): Validates timeline event dates (lines 345-350)

⚠️ Not Implemented (Optional Enhancements)

  1. Documentation README: No README.md in .github/actions/stale-unassign/
  2. Enhanced job summary: Basic summary exists (lines 408-416), but missing configuration details (grace period, labels, dry-run status)
  3. Re-labeling edge case logging: No log message when labelEvents.length > 1

🔬 TEST COVERAGE & FLAKINESS ANALYSIS

No Automated Tests Found

Finding: The action has zero automated test files (no .test.js, .spec.js, or test workflows).

Implications:

  • Manual testing capabilities exist: Dry-run mode (dry-run: true) and minutes override (grace-period-minutes: 5) allow safe manual validation
  • Tested in production: PR description states "tested with a 5-minute grace period in talawa-admin"
  • No CI/CD test coverage: Changes can't be validated automatically
  • No regression protection: Future modifications could break existing behavior

Recommendation: While the action is functional, adding tests would improve maintainability. However, since this is a composite action with inline GitHub Script, traditional unit testing is challenging. Acceptable alternatives:

  • Integration tests: Create test issues/PRs in a sandbox repo and run the action
  • Mock tests: Use @actions/github mock in a separate .test.js file
  • Documented manual test procedure: Checklist for manual validation before releases

🟡 Potential Flakiness Concerns

1. GitHub API Rate Limiting (Medium Risk)

Location: Lines 201-230 (issue fetching), 268 (timeline fetching)

Risk: In repositories with many stale issues, the action could hit rate limits despite retry logic.

Mitigations present:

  • max-issues-per-run limit (default: 50)
  • paginate.iterator with early break (commit 929b6fb)
  • ✅ Exponential backoff retry with Retry-After header parsing (lines 40-75)

Flakiness verdict: Low risk in production. The max-issues-per-run cap and retry logic make this robust.


2. Timeline Event Pagination (Low Risk)

Location: Lines 268-276

Risk: For issues with thousands of events, pagination could be slow or fail mid-stream.

Mitigations present:

  • ✅ Uses github.paginate which handles pagination automatically
  • ✅ Retry wrapper catches network/server errors
  • ✅ Per-issue error handling (lines 242-406) prevents one failure from breaking the entire run

Flakiness verdict: Very low risk. Error handling is comprehensive.


3. Date Parsing Edge Cases (Very Low Risk)

Location: Lines 336-350

Risk: Invalid created_at timestamps from GitHub API could cause crashes.

Mitigations present:

  • ✅ Validates date is not NaN (lines 345-350, added in commit 78e44c7)
  • ✅ Skips issue with clear log message if invalid

Flakiness verdict: Negligible risk. Validation added.


4. Cross-Repository PR Detection (Low Risk)

Location: Lines 286-301

Risk: Regex parsing of repository_url could fail if GitHub API changes URL format.

Mitigations present:

  • ✅ Null checks for missing repository_url (lines 291-294)
  • ✅ Logs warnings for unparseable URLs (line 298)
  • ⚠️ Treats parsing failures as "different repo" (safe default, but might miss same-repo PRs)

Flakiness verdict: Low risk, but could benefit from defensive logging.

Suggestion:

if (!urlMatch) {
  console.warn(`Issue #${issue.number}: Unexpected repository_url format: ${prRepoUrl}, treating as different repo`);
}

This would help diagnose API format changes.


5. Sharded Testing Environment Concerns (N/A)

Finding: The action does not use mocks and has no test suite, so sharded testing concerns don't apply.

If tests were added in the future:

  • Use nock or similar to mock GitHub API responses
  • Ensure mock cleanup in afterEach() blocks
  • Use unique issue numbers per test to avoid collisions in parallel runs
  • Mock system time (Date.now()) to avoid time-dependent flakiness

📊 CODE QUALITY ASSESSMENT

Strengths

  1. Robust error handling: Retry logic with exponential backoff, Retry-After header support (both numeric and HTTP-date formats)
  2. Security: Environment variables prevent script injection
  3. Input validation: All inputs validated with defaults and warnings
  4. Graceful degradation: Per-issue errors don't fail entire workflow
  5. Safety features: Dry-run mode, max limits, archived repo check
  6. Performance optimization: Early pagination break, batch assignee removal

⚠️ Areas for Improvement

1. Documentation Gap (Medium Priority)

Missing: README in .github/actions/stale-unassign/

Should include:

  • Use case examples
  • Input parameter explanations
  • Label workflow diagram
  • Troubleshooting guide
  • Testing instructions

Impact: Users might misconfigure the action without clear documentation.


2. Job Summary Limited (Low Priority)

Current output:

| Metric     | Count |
|------------|-------|
| Processed  | 5     |
| Skipped    | 12    |
| Errors     | 0     |

Could include:

  • Configuration used (grace period, stale label, exclusion labels)
  • Dry-run status
  • List of processed issue numbers
  • Reasons for skips (breakdown: "no assignee", "has exclusion label", "grace period not met", etc.)

Impact: Minimal. Current summary is functional but could be more informative.


3. Re-labeling Not Logged (Very Low Priority)

Location: Lines 324-342

Current behavior: Uses most recent label event (correct), but doesn't log when multiple events exist.

Suggestion:

if (labelEvents.length > 1) {
  console.log(`Issue #${issue.number}: Stale label was re-applied ${labelEvents.length} times, using most recent (${labelAddedDate.toISOString()})`);
}

Impact: Helps diagnose issues where labels are removed and re-added.


🔒 SECURITY REVIEW

No security concerns identified:

  • Environment variables prevent script injection
  • No use of eval() or dynamic code execution
  • Token permissions properly scoped (issues: write, contents: read, pull-requests: read)
  • No exposure of secrets in logs
  • Archived repo check prevents unintended runs

🎓 PRODUCTION READINESS

Category Rating Notes
Functionality Excellent All requirements met
Error Handling Excellent Comprehensive retry logic, rate limiting
Security Strong No vulnerabilities identified
Documentation ⚠️ Fair Comments in code are good, but no README
Test Coverage None No automated tests (manual testing exists)
Flakiness Risk Low Good mitigations for API rate limits, errors
Maintainability ⚠️ Good Well-structured, but would benefit from tests

🚀 FINAL VERDICT & RECOMMENDATIONS

APPROVED FOR PRODUCTION

The action is functionally complete, secure, and robust. All critical issues from previous reviews are resolved, and all issue #24 requirements are met.

📋 Recommendations Before Merge (Optional)

  1. Add README.md (15 minutes):

    # Stale Issue Unassigner
    
    Automatically unassigns stale issues after a grace period.
    
    ## Inputs
    - `stale-label`: Triggers countdown (default: `no-issue-activity`)
    - `exclusion-labels`: Prevents unassignment (comma-separated)
    - `grace-period-days`: Days before unassign (default: 7)
    - `grace-period-minutes`: Minutes override for testing (default: 0)
    ...
    
    ## Example
    ```yaml
    - uses: ./.github/actions/stale-unassign
      with:
        exclusion-labels: 'waiting-for-maintainer,blocked'
        grace-period-days: '14'
    
    
  2. Enhance job summary (10 minutes):
    Add configuration details to the summary table.

  3. Add re-labeling log (2 minutes):
    Log when multiple label events detected.

📋 Future Enhancements (Post-Merge)

  1. Add integration tests: Create test workflow in sandbox repo
  2. Metrics collection: Track unassignment trends over time
  3. Configurable comment per repo: Allow repos to override default message

📝 SUMMARY

All issue #24 goals achieved
All critical CodeRabbit findings resolved
Low flakiness risk due to comprehensive error handling
⚠️ No automated tests, but dry-run/manual testing capabilities exist
⚠️ Documentation could be improved with README

The action is production-ready and can be merged. The missing tests and documentation are nice-to-have improvements, not blockers.

Excellent work, @Ritesh-251! The implementation is solid and well-thought-out. 🎉


✅ Actions performed

Full review triggered.

@github-actions
Copy link

This pull request did not get any activity in the past 10 days and will be closed in 360 days if no update occurs. Please verify it has no conflicts with the develop branch and rebase if needed.

@Ritesh-251
Copy link
Author

@palisadoes this req has been pending since weeks !! pls look into it why it took so much time

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