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
Also, I’m confused about how to fetch Trac details like ‘ticket type’
I really need guidance.