From 7871a51434dd82fd5696d01eff333d8353220687 Mon Sep 17 00:00:00 2001 From: Laszlo <47461634+Lacah@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:06:56 +0200 Subject: [PATCH 1/4] Swapping to call shared workflow --- .github/workflows/pr-auto-unassign-stale.yml | 157 ++----------------- 1 file changed, 13 insertions(+), 144 deletions(-) diff --git a/.github/workflows/pr-auto-unassign-stale.yml b/.github/workflows/pr-auto-unassign-stale.yml index 5bc93bc475..d1629495c7 100644 --- a/.github/workflows/pr-auto-unassign-stale.yml +++ b/.github/workflows/pr-auto-unassign-stale.yml @@ -1,154 +1,23 @@ -name: Auto-unassign stale PR assignees - +name: Unassign stale PR assignees on: schedule: - - cron: "*/15 * * * *" # run every 15 minutes + - cron: "*/15 * * * *" workflow_dispatch: inputs: - enabled: - description: "Enable this automation" - type: boolean - default: true - max_age_minutes: - description: "Unassign if assigned longer than X minutes" - type: number - default: 60 - dry_run: - description: "Preview only; do not change assignees" - type: boolean - default: false + enabled: { type: boolean, default: true } + max_age_minutes: { type: number, default: 60 } + dry_run: { type: boolean, default: false } permissions: pull-requests: write issues: write - -env: - # Defaults (can be overridden via workflow_dispatch inputs) - ENABLED: "true" - MAX_ASSIGN_AGE_MINUTES: "60" - DRY_RUN: "false" + contents: read jobs: - sweep: - runs-on: ubuntu-latest - steps: - - name: Resolve inputs into env - run: | - # Prefer manual run inputs when present - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "ENABLED=${{ inputs.enabled }}" >> $GITHUB_ENV - echo "MAX_ASSIGN_AGE_MINUTES=${{ inputs.max_age_minutes }}" >> $GITHUB_ENV - echo "DRY_RUN=${{ inputs.dry_run }}" >> $GITHUB_ENV - fi - echo "Effective config: ENABLED=$ENABLED, MAX_ASSIGN_AGE_MINUTES=$MAX_ASSIGN_AGE_MINUTES, DRY_RUN=$DRY_RUN" - - - name: Exit if disabled - if: ${{ env.ENABLED != 'true' && env.ENABLED != 'True' && env.ENABLED != 'TRUE' }} - run: echo "Disabled via ENABLED=$ENABLED. Exiting." && exit 0 - - - name: Unassign stale assignees - uses: actions/github-script@v7 - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - - const MAX_MIN = parseInt(process.env.MAX_ASSIGN_AGE_MINUTES || "60", 10); - const DRY_RUN = ["true","True","TRUE","1","yes"].includes(String(process.env.DRY_RUN)); - const now = new Date(); - - core.info(`Scanning open PRs. Threshold = ${MAX_MIN} minutes. DRY_RUN=${DRY_RUN}`); - - // List all open PRs - const prs = await github.paginate(github.rest.pulls.list, { - owner, repo, state: "open", per_page: 100 - }); - - let totalUnassigned = 0; - - for (const pr of prs) { - if (!pr.assignees || pr.assignees.length === 0) continue; - - const number = pr.number; - core.info(`PR #${number}: "${pr.title}" — assignees: ${pr.assignees.map(a => a.login).join(", ")}`); - - // Pull reviews (to see if an assignee started a review) - const reviews = await github.paginate(github.rest.pulls.listReviews, { - owner, repo, pull_number: number, per_page: 100 - }); - - // Issue comments (general comments) - const issueComments = await github.paginate(github.rest.issues.listComments, { - owner, repo, issue_number: number, per_page: 100 - }); - - // Review comments (file-level) - const reviewComments = await github.paginate(github.rest.pulls.listReviewComments, { - owner, repo, pull_number: number, per_page: 100 - }); - - // Issue events (to find assignment timestamps) - const issueEvents = await github.paginate(github.rest.issues.listEvents, { - owner, repo, issue_number: number, per_page: 100 - }); - - for (const a of pr.assignees) { - const assignee = a.login; - - // Find the most recent "assigned" event for this assignee - const assignedEvents = issueEvents - .filter(e => e.event === "assigned" && e.assignee && e.assignee.login === assignee) - .sort((x, y) => new Date(y.created_at) - new Date(x.created_at)); - - if (assignedEvents.length === 0) { - core.info(` - @${assignee}: no 'assigned' event found; skipping.`); - continue; - } - - const assignedAt = new Date(assignedEvents[0].created_at); - const ageMin = (now - assignedAt) / 60000; - - // Has the assignee commented (issue or review comments) or reviewed? - const hasIssueComment = issueComments.some(c => c.user?.login === assignee); - const hasReviewComment = reviewComments.some(c => c.user?.login === assignee); - const hasReview = reviews.some(r => r.user?.login === assignee); - - const eligible = - ageMin >= MAX_MIN && - !hasIssueComment && - !hasReviewComment && - !hasReview && - pr.state === "open"; - - core.info(` - @${assignee}: assigned ${ageMin.toFixed(1)} min ago; commented=${hasIssueComment || hasReviewComment}; reviewed=${hasReview}; open=${pr.state==='open'} => ${eligible ? 'ELIGIBLE' : 'skip'}`); - - if (!eligible) continue; - - if (DRY_RUN) { - core.notice(`Would unassign @${assignee} from PR #${number}`); - } else { - try { - await github.rest.issues.removeAssignees({ - owner, repo, issue_number: number, assignees: [assignee] - }); - totalUnassigned += 1; - // Optional: leave a gentle heads-up comment - await github.rest.issues.createComment({ - owner, repo, issue_number: number, - body: `👋 Unassigning @${assignee} due to inactivity (> ${MAX_MIN} min without comments/reviews). This PR remains open for other reviewers.` - }); - core.info(` Unassigned @${assignee} from #${number}`); - } catch (err) { - core.warning(` Failed to unassign @${assignee} from #${number}: ${err.message}`); - } - } - } - } - - core.summary - .addHeading('Auto-unassign report') - .addRaw(`Threshold: ${MAX_MIN} minutes\n\n`) - .addRaw(`Total unassignments: ${totalUnassigned}\n`) - .write(); - - result-encoding: string + call: + uses: ServiceNowDevProgram/Hacktoberfest/.github/workflows/unassign-stale.yml@v1 + with: + enabled: ${{ inputs.enabled }} + max_age_minutes: ${{ inputs.max_age_minutes }} + dry_run: ${{ inputs.dry_run }} + secrets: inherit From 7e6014e43c921a0f14ab53ff740f1ae62249c1bd Mon Sep 17 00:00:00 2001 From: Laszlo <47461634+Lacah@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:07:27 +0200 Subject: [PATCH 2/4] Rename pr-auto-unassign-stale.yml to unassign-stale.yml --- .../pr-auto-unassign-stale.yml => workflow/unassign-stale.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows/pr-auto-unassign-stale.yml => workflow/unassign-stale.yml} (100%) diff --git a/.github/workflows/pr-auto-unassign-stale.yml b/.github/workflow/unassign-stale.yml similarity index 100% rename from .github/workflows/pr-auto-unassign-stale.yml rename to .github/workflow/unassign-stale.yml From cfaa9370055786bc38b365dd65243761f08f8d4d Mon Sep 17 00:00:00 2001 From: Laszlo <47461634+Lacah@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:59:18 +0200 Subject: [PATCH 3/4] Delete .github/workflow/unassign-stale.yml --- .github/workflow/unassign-stale.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflow/unassign-stale.yml diff --git a/.github/workflow/unassign-stale.yml b/.github/workflow/unassign-stale.yml deleted file mode 100644 index d1629495c7..0000000000 --- a/.github/workflow/unassign-stale.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Unassign stale PR assignees -on: - schedule: - - cron: "*/15 * * * *" - workflow_dispatch: - inputs: - enabled: { type: boolean, default: true } - max_age_minutes: { type: number, default: 60 } - dry_run: { type: boolean, default: false } - -permissions: - pull-requests: write - issues: write - contents: read - -jobs: - call: - uses: ServiceNowDevProgram/Hacktoberfest/.github/workflows/unassign-stale.yml@v1 - with: - enabled: ${{ inputs.enabled }} - max_age_minutes: ${{ inputs.max_age_minutes }} - dry_run: ${{ inputs.dry_run }} - secrets: inherit From 8391dfb70cabdc0f5c30760c648f9f0a0370d38c Mon Sep 17 00:00:00 2001 From: Laszlo <47461634+Lacah@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:19:51 +0200 Subject: [PATCH 4/4] Create unassign-stale.yml Switching the automation to shim that calls the central one --- .github/workflows/unassign-stale.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/unassign-stale.yml diff --git a/.github/workflows/unassign-stale.yml b/.github/workflows/unassign-stale.yml new file mode 100644 index 0000000000..a372463c60 --- /dev/null +++ b/.github/workflows/unassign-stale.yml @@ -0,0 +1,23 @@ +name: Unassign stale PR assignees +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + inputs: + enabled: { type: boolean, default: true } + max_age_minutes: { type: number, default: 60 } + dry_run: { type: boolean, default: false } + +permissions: + pull-requests: write + issues: write + contents: read + +jobs: + call: + uses: ServiceNowDevProgram/Hacktoberfest/.github/workflows/unassign-stale.yml@main + with: + enabled: ${{ inputs.enabled }} + max_age_minutes: ${{ inputs.max_age_minutes }} + dry_run: ${{ inputs.dry_run }} + secrets: inherit