Changes in automated PRs management on GitHub

Hello again. I’m working on updating Django https://github.com/django/django/tree/main/.github/workflows pipeline.

As per suggestions I got and inputs I gathered. I’m working on three updates.

I have drafted yml pipeline for these.

1. Validating the ticket labeling

name: Trac Ticket Validation
on:
  pull_request_target:
    types: [edited, opened, reopened, ready_for_review]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true


permissions:
  pull-requests: write
  issues: write
jobs:
  ticket_validation:
    name: Validate Trac Tickets and apply labels
    runs-on: ubuntu-latest
    steps:
      - name: Validate and label
        uses: actions/github-script@v7
        env:
          TRAC_API: "https://code.djangoproject.com/jsonrpc"
        with:
          script: |
            const { title, number, labels } = context.payload.pull_request;
            const owner = context.repo.owner;
            const repo = context.repo.repo;


            // Match Django's official title patterns for ticket references
            const ticketMatch = title.match(/(?:close[sd]?|fixe[sd]?|refs)\s+#?(\d+)/i);
            const ticketId = ticketMatch?.[1];


            // Define labels for different ticket types
            const TYPE_LABELS = {
              'defect': 'type: Bug',
              'enhancement': 'type: New Feature',
              'task': 'type: Cleanup/Optimization'
            };
            async function validateTicket() {
              if (!ticketId) {
                if (!labels.some(l => l.name === 'no ticket')) {
                  await addLabel('no ticket');
                }
                return;
              }

              try {
                // NEED HELP: To confirm the correct Trac API calling


                // Make a JSON-RPC request to the Trac API to fetch ticket details
                const response = await fetch(process.env.TRAC_API, {
                  method: 'POST',
                  headers: { 'Content-Type': 'application/json' },
                  body: JSON.stringify({
                    jsonrpc: "2.0",
                    method: "ticket.get",
                    params: [parseInt(ticketId)],
                    id: 1
                  })
                });
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
               
                const data = await response.json();
                // Handle errors in the JSON-RPC response
                if (data.error) {
                  await handleInvalidTicket(ticketId, data.error.message);
                  return;
                }
                // Validate the response structure and extract the ticket type
                if (!data.result?.[3]?.type) {
                  throw new Error('Invalid ticket data structure');
                }
                const ticketType = data.result[3].type.toLowerCase();
                await handleValidTicket(ticketType);
              } catch (error) {
                console.error('Validation failed:', error);
                await handleInvalidTicket(ticketId, error.message);
              }
            }
            async function handleInvalidTicket(ticketId, reason) {
              await removeLabel('no ticket');
              if (!labels.some(l => l.name === 'invalid ticket')) {
                await addLabel('invalid ticket');
              }
              await postComment(
                `**Ticket Validation Failed**\n` +
                `Could not verify #${ticketId}: ${reason}\n` +
                `• Verify ticket exists: https://code.djangoproject.com/ticket/${ticketId}\n` +
                `• Create new ticket: https://code.djangoproject.com/newticket`
              );
            }
            # Helper functions
            async function handleValidTicket(ticketType) {
              await removeLabel('no ticket');
              await removeLabel('invalid ticket');
             
              const label = TYPE_LABELS[ticketType] || 'type: Other';
              await syncLabels(label);
            }
            async function syncLabels(newLabel) {
              const typePrefix = 'type: ';
              const currentLabels = labels.map(l => l.name);
             
              // Remove existing type labels
              for (const label of currentLabels.filter(n => n.startsWith(typePrefix))) {
                if (label !== newLabel) await removeLabel(label);
              }
             
              // Add new label if needed
              if (!currentLabels.includes(newLabel)) {
                await addLabel(newLabel);
              }
            }
            async function addLabel(label) {
              await github.rest.issues.addLabels({
                owner, repo, issue_number: number,
                labels: [label]
              });
            }
            async function removeLabel(label) {
              try {
                await github.rest.issues.removeLabel({
                  owner, repo, issue_number: number,
                  name: label
                });
              } catch (error) {
                if (error.status !== 404) throw error;
              }
            }
            async function postComment(message) {
              await github.rest.issues.createComment({
                owner, repo, issue_number: number,
                body: message
              });
            }


            await validateTicket();

2.Stale PRs monitoring

name: Stale PR Management

on:
  schedule:
    - cron: '0 0 * * 1'  # Runs every Monday at midnight UTC

permissions:
  pull-requests: write
  issues: write


jobs:
  check_stale_prs:
    name: Check and Manage Stale PRs
    runs-on: ubuntu-latest
    steps:
      - name: Fetch and Process PRs
        uses: actions/github-script@v7
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const now = new Date();

            // Helper function to calculate a date before now
            function getDateBeforeNow(months = 0, weeks = 0, days = 0) {
              const date = new Date(now);
              date.setMonth(date.getMonth() - months);
              date.setDate(date.getDate() - (weeks * 7) - days);
              return date;
            }


            // Inactivity thresholds
            const threeMonthsBeforeNow = getDateBeforeNow(3);
            const fourMonthsBeforeNow = getDateBeforeNow(4);
            const fourMonthsAndOneWeekBeforeNow = getDateBeforeNow(4, 1); // 4 months + 1 week

            // Helper function to paginate open PRs
            async function getAllOpenPRs() {
              let allPRs = [];
              let page = 1;
              const perPage = 100;
              while (true) {
                const { data: prs } = await github.rest.pulls.list({
                  owner,
                  repo,
                  state: "open",
                  per_page: perPage,
                  page: page
                });
                if (prs.length === 0) break;
                allPRs = allPRs.concat(prs);
                page++;
              }
              return allPRs;
            }


            async function processPRs() {
              const pullRequests = await getAllOpenPRs();
              console.log(`Total PRs fetched: ${pullRequests.length}`);
             
              for (const pr of pullRequests) {
                const prNumber = pr.number;
                const updatedAt = new Date(pr.updated_at);

                // Fetch PR labels
                const { data: labels } = await github.rest.issues.listLabelsOnIssue({
                  owner,
                  repo,
                  issue_number: prNumber
                });
                const labelNames = labels.map(l => l.name);


                // Fetch PR timeline to check for contributor activity (comments & events)
                const { data: timeline } = await github.rest.issues.listEvents({
                  owner,
                  repo,
                  issue_number: prNumber,
                  per_page: 100
                });
                const lastContributorActivity = timeline
                  .filter(event => event.actor && event.actor.type === "User")
                  .map(event => new Date(event.created_at))
                  .sort((a, b) => b - a)[0] || updatedAt;


                // Rule 1: 3 months inactivity → Add reminder
                if (lastContributorActivity < threeMonthsBeforeNow && !labelNames.includes('stale-notice')) {
                  await github.rest.issues.createComment({
                    owner,
                    repo,
                    issue_number: prNumber,
                    body: `👋 This PR has been inactive for 3 months. Are you still working on this? If you're facing issues, please update the PR or discuss them in our forum.`
                  });
                  await github.rest.issues.addLabels({
                    owner,
                    repo,
                    issue_number: prNumber,
                    labels: ['stale-notice']
                  });
                  console.log(`PR #${prNumber} marked with stale-notice.`);
                }


                // Rule 2: If contributor responds after the reminder, remove stale-notice
                if (labelNames.includes('stale-notice') && lastContributorActivity > threeMonthsBeforeNow) {
                  try {
                    await github.rest.issues.removeLabel({
                      owner,
                      repo,
                      issue_number: prNumber,
                      name: 'stale-notice'
                    });
                    console.log(`PR #${prNumber} is active again. Removed stale-notice.`);
                  } catch (e) {
                    // Ignore if label not found
                  }
                }

                // Rule 3: 4 months inactivity → Add warning
                if (lastContributorActivity < fourMonthsBeforeNow && !labelNames.includes('need-attention')) {
                  await github.rest.issues.createComment({
                    owner,
                    repo,
                    issue_number: prNumber,
                    body: `⚠️ This PR has been inactive for 4 months. If no action is taken soon, it may be marked as stale and closed.`
                  });
                  await github.rest.issues.addLabels({
                    owner,
                    repo,
                    issue_number: prNumber,
                    labels: ['need-attention']
                  });
                  console.log(`PR #${prNumber} marked with need-attention.`);
                }

                // Rule 4: 4 months + 1 week inactivity after warning → Mark as stale & close
                if (lastContributorActivity < fourMonthsAndOneWeekBeforeNow && labelNames.includes('need-attention')) {
                  await github.rest.issues.createComment({
                    owner,
                    repo,
                    issue_number: prNumber,
                    body: `🚨 This PR has been inactive for too long. Closing due to inactivity. If you are still working on this, please reopen the PR or ask a maintainer for help.`
                  });
                  await github.rest.issues.addLabels({
                    owner,
                    repo,
                    issue_number: prNumber,
                    labels: ['stale']
                  });
                  await github.rest.pulls.update({
                    owner,
                    repo,
                    pull_number: prNumber,
                    state: "closed"
                  });
                  console.log(`PR #${prNumber} marked as stale and closed.`);
                }
              }
            }

            await processPRs();

3. Faster and Automated PR Feedback and Review Process

WIP…
any suggestions for theoretical approach?

I am requesting seniors and mentors to check it and guide me whether my idea is feasible and my approach is correct :pleading_face:
Also, I’m confused about how to fetch Trac details like ‘ticket type’

I really need guidance.