Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/scripts/pr_inactivity_reminder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// A script to remind PR authors of inactivity by posting a comment.

// DRY_RUN env var: any case-insensitive 'true' value will enable dry-run
const dryRun = (process.env.DRY_RUN || 'false').toString().toLowerCase() === 'true';

// Helper to resolve the head repo of a PR
function resolveHeadRepo(pr, defaultOwner, defaultRepo) {
return {
owner: pr.head.repo?.owner?.login || defaultOwner,
repo: pr.head.repo?.name || defaultRepo,
};
}

// Helper to get the last commit date of a PR
async function getLastCommitDate(github, pr, owner, repo) {
const { owner: headRepoOwner, repo: headRepoName } = resolveHeadRepo(pr, owner, repo);
try {
const commitRes = await github.rest.repos.getCommit({
owner: headRepoOwner,
repo: headRepoName,
ref: pr.head.sha,
});
const commit = commitRes.data?.commit ?? null;
return new Date(commit?.author?.date || commit?.committer?.date || pr.created_at);
} catch (getCommitErr) {
console.log(`Failed to fetch head commit ${pr.head.sha} for PR #${pr.number}:`, getCommitErr.message || getCommitErr);
return null; // Signal fallback needed
}
}


// Look for an existing bot comment using our unique marker.
async function hasExistingBotComment(github, pr, owner, repo, marker) {
try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pr.number,
per_page: 100,
});
return comments.find(c => c.body && c.body.includes(marker)) || false;
} catch (err) {
console.log(`Failed to list comments for PR #${pr.number}:`, err.message || err);
return null; // Prevent duplicate comment if we cannot check
}
}

// Helper to post an inactivity comment
async function postInactivityComment(github, pr, owner, repo, marker, inactivityThresholdDays, discordLink, office_hours_calendar) {
const comment = `${marker}
Hi @${pr.user.login},\n\nThis pull request has had no commit activity for ${inactivityThresholdDays} days. Are you still working on the issue? please push a commit to keep the PR active or it will be closed due to inactivity.
Reach out on discord or join our office hours if you need assistance.\n\n- ${discordLink}\n- ${office_hours_calendar} \n\nFrom the Python SDK Team`;
if (dryRun) {
console.log(`DRY-RUN: Would comment on PR #${pr.number} (${pr.html_url}) with body:\n---\n${comment}\n---`);
return true;
}

try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: comment,
});
console.log(`Commented on PR #${pr.number} (${pr.html_url})`);
return true;
} catch (commentErr) {
console.log(`Failed to comment on PR #${pr.number}:`, commentErr);
return false;
}
}

// Main module function
module.exports = async ({github, context}) => {
const inactivityThresholdDays = 10; // days of inactivity before commenting
const cutoff = new Date(Date.now() - inactivityThresholdDays * 24 * 60 * 60 * 1000);
const owner = context.repo.owner;
const repo = context.repo.repo;
const discordLink = `[Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)`;
const office_hours_calendar =`[Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week)`;
// Unique marker so we can find the bot's own comment later.
const marker = '<!-- pr-inactivity-bot-marker -->';

if (dryRun) {
console.log('Running in DRY-RUN mode: no comments will be posted.');
}

let commentedCount = 0;
let skippedCount = 0;

const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});

for (const pr of prs) {
// 1. Check inactivity
const lastCommitDate = await getLastCommitDate(github, pr, owner, repo);
if (lastCommitDate > cutoff) {
skippedCount++;
console.log(`PR #${pr.number} has recent commit on ${lastCommitDate.toISOString()} - skipping`);
continue;
}

// 2. Check for existing comment
const existingBotComment = await hasExistingBotComment(github, pr, owner, repo, marker);
if (existingBotComment) {
skippedCount++;
const idInfo = existingBotComment && existingBotComment.id ? existingBotComment.id : '(unknown)';
console.log(`PR #${pr.number} already has an inactivity comment (id: ${idInfo}) - skipping`);
continue;
}

// 3. Post inactivity comment
const commented = await postInactivityComment(github, pr, owner, repo, marker, inactivityThresholdDays, discordLink, office_hours_calendar);
if (commented) commentedCount++;
}

console.log("=== Summary ===");
console.log(`PRs commented: ${commentedCount}`);
console.log(`PRs skipped (existing comment present): ${skippedCount}`);
};
42 changes: 42 additions & 0 deletions .github/workflows/pr-inactivity-reminder-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This workflow warns PRs that have had no activity for a specified amount of time(10 Days).

name: PR Inactivity Reminder Bot

on:
schedule:
- cron: '0 11 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'If true, do not post comments (dry run). Accepts "true" or "false". Default true for manual runs.'
required: false
default: 'true'

permissions:
pull-requests: write
issues: write
contents: read


jobs:
remind_inactive_prs:
runs-on: ubuntu-latest
env:
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}

steps:
- name: Harden the runner
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Remind authors of inactive PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ env.DRY_RUN }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const script = require('./.github/scripts/pr_inactivity_reminder.js')
await script({ github, context });
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- feat: AccountCreateTransaction now supports both PrivateKey and PublicKey [#939](https://github.com/hiero-ledger/hiero-sdk-python/issues/939)
- Added Acceptance Criteria section to Good First Issue template for better contributor guidance (#997)
- Added __str__() to CustomRoyaltyFee and updated examples and tests accordingly (#986)
- Add PR inactivity reminder bot for stale pull requests `.github/workflows/pr-inactivity-reminder-bot.yml`

### Changed

Expand Down