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 (daily schedule or manual). 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, or issues with open linked PRs, and emits 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 to unassign issues labeled as 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-minutes, grace-period-days, dry-run, stale-label, exclusion-labels, comment-message, max-issues-per-run, github-token). Implements input normalization/validation, archived-repo and label checks, capped issue listing, timeline parsing to detect linked PRs/authors, grace-period computation (minutes take precedence), conditional unassignment and templated commenting, per-issue error isolation, aggregated core.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 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 'Add stale unassign GitHub Action' is clear, specific, and accurately summarizes the main change—introducing a new GitHub Action for automatically unassigning stale issues.
Linked Issues check ✅ Passed The pull request implements all coding requirements from issue #24: checks archived status, identifies stale issues, verifies assignees, checks for open linked PRs by assignees, enforces grace periods (days/minutes), unassigns after grace period, posts comments, supports exclusion labels, and is reusable from PalisadoesFoundation/.github.
Out of Scope Changes check ✅ Passed All changes are in scope: the action.yml file defines the composite action with stale-unassign logic, and stale-unassign.yml orchestrates its execution. Both files directly support the objective to automate unassignment of stale issues.
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

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