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.
