Skip to content
Merged
Changes from all 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
244 changes: 78 additions & 166 deletions .github/workflows/autolabel-pr-issue.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Auto Label PR from Linked Issue
name: Sync Issue Metadata to PR

on:
pull_request_target:
Expand All @@ -8,211 +8,123 @@ permissions:
pull-requests: write
issues: write
contents: read
repository-projects: write

jobs:
label-pr:
sync-pr-metadata:
runs-on: ubuntu-latest

steps:
- name: Checkout code
- name: Checkout
uses: actions/checkout@v4

- name: Extract Issue Numbers from PR Body
- name: Extract linked issue(s) from PR
id: extract-issues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-encoding: string
script: |
let prNumber, prBody, prTitle;

// Check if triggered by issue event
if (context.eventName === 'issues') {
// Find all open PRs that link to this issue
const issueNumber = context.payload.issue.number;
console.log(`Issue #${issueNumber} labels were updated`);

// Search for PRs that mention this issue
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});

const linkedPRs = [];
for (const pr of pullRequests) {
const prText = `${pr.title} ${pr.body || ''}`;
const patterns = [
new RegExp(`(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueNumber}\\b`, 'i'),
new RegExp(`#${issueNumber}\\b`)
];

if (patterns.some(p => p.test(prText))) {
linkedPRs.push(pr.number);
}
}

if (linkedPRs.length === 0) {
console.log('No linked PRs found for this issue');
return JSON.stringify({ prs: [], issue: issueNumber });
const prNumber = context.payload.pull_request.number;
const prTitle = context.payload.pull_request.title || '';
const prBody = context.payload.pull_request.body || '';

// Regex patterns for issue references
const patterns = [
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
/#(\d+)/g
];

const issueNumbers = new Set();
const text = prTitle + ' ' + prBody;

for (const pattern of patterns) {
for (const match of text.matchAll(pattern)) {
issueNumbers.add(match[1]);
}

console.log(`Found linked PRs: ${linkedPRs.join(', ')}`);
return JSON.stringify({ prs: linkedPRs, issue: issueNumber });
} else {
// Triggered by PR event - original logic
prBody = context.payload.pull_request.body || '';
prTitle = context.payload.pull_request.title || '';

const patterns = [
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
/#(\d+)/g
];

const issueNumbers = new Set();
const textToSearch = prBody + ' ' + prTitle;

patterns.forEach(pattern => {
const matches = [...textToSearch.matchAll(pattern)];
matches.forEach(match => {
issueNumbers.add(match[1]);
});
});

const issues = Array.from(issueNumbers);
console.log('Found linked issues:', issues);

return JSON.stringify({
prs: [context.payload.pull_request.number],
issues: issues
});
}

- name: Get Labels from Linked Issues
id: get-labels
return JSON.stringify({ issues: Array.from(issueNumbers), pr: prNumber });

- name: Sync Issue Metadata to PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-encoding: string
script: |
const extractData = JSON.parse('${{ steps.extract-issues.outputs.result }}');

// Labels to exclude from being applied to PRs
const excludedLabels = ['recode', 'hacktoberfest-accepted'];

let issueNumbers = [];
let prsToUpdate = [];

// Handle both PR and issue events
if (extractData.issue) {
// Issue event - update all linked PRs
issueNumbers = [extractData.issue];
prsToUpdate = extractData.prs || [];
} else {
// PR event - update the current PR
issueNumbers = extractData.issues || [];
prsToUpdate = extractData.prs || [];
}

if (!issueNumbers || issueNumbers.length === 0) {
console.log('No linked issues found');
return JSON.stringify({ labels: [], prs: prsToUpdate });
const data = JSON.parse('${{ steps.extract-issues.outputs.result }}');
const prNumber = data.pr;
const issueNumbers = data.issues || [];

if (issueNumbers.length === 0) {
console.log("No linked issues found");
return;
}

const allLabels = new Set();


for (const issueNumber of issueNumbers) {
try {
const issue = await github.rest.issues.get({
// Fetch issue
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(issueNumber)
});

console.log(`Issue #${issueNumber} labels:`, issue.data.labels.map(l => l.name));

issue.data.labels.forEach(label => {
// Only add label if it's not in the excluded list
if (!excludedLabels.includes(label.name.toLowerCase())) {
allLabels.add(label.name);
} else {
console.log(`Excluding label: ${label.name}`);
}
});
} catch (error) {
console.log(`Could not fetch issue #${issueNumber}:`, error.message);
}
}

const labels = Array.from(allLabels);
console.log('All labels to apply:', labels);

return JSON.stringify({ labels: labels, prs: prsToUpdate });

- name: Apply Labels to PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const data = JSON.parse('${{ steps.get-labels.outputs.result }}');
const labels = data.labels || [];
const prsToUpdate = data.prs || [];

if (!labels || labels.length === 0) {
console.log('No labels to apply');
return;
}

if (!prsToUpdate || prsToUpdate.length === 0) {
console.log('No PRs to update');
return;
}

// Update each PR
for (const prNumber of prsToUpdate) {
try {
// First, get current PR labels
console.log(`Syncing metadata from Issue #${issueNumber} to PR #${prNumber}`);

// --- Sync Labels ---
const issueLabels = issue.labels.map(l => l.name);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});

// Remove all existing labels first (to handle removed issue labels)
const currentLabels = pr.labels.map(l => l.name);
if (currentLabels.length > 0) {
for (const label of currentLabels) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: label
});
} catch (e) {
console.log(`Could not remove label ${label}: ${e.message}`);
}
}
}

// Apply new labels
const currentPRLabels = pr.labels.map(l => l.name);
const combinedLabels = Array.from(new Set([...currentPRLabels, ...issueLabels]));

await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: labels
labels: combinedLabels
});

console.log(`Successfully applied ${labels.length} labels to PR #${prNumber}`);

// Add a comment to the PR
console.log(`Labels applied: ${combinedLabels.join(', ')}`);

// --- Sync Milestone ---
if (issue.milestone) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
milestone: issue.milestone.number
});
console.log(`Milestone synced: ${issue.milestone.title}`);
}

// --- Sync Projects (GitHub Projects v2) ---
if(issue.project_cards_url) {
// Fetch project cards of issue
const cardsResponse = await github.rest.projects.listCards({
column_id: issue.project_cards_url.split('/').pop() // last part is column_id
}).catch(()=>({data:[]}));

for(const card of cardsResponse.data || []) {
await github.rest.projects.createCard({
column_id: card.column_id,
content_id: prNumber,
content_type: 'PullRequest'
});
console.log(`Added PR #${prNumber} to project card in column ${card.column_id}`);
}
}

// --- Optionally: Add a comment on PR ---
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `🏷️ Labels automatically synced from linked issue(s): ${labels.map(l => `\`${l}\``).join(', ')}`
body: `✅ Synchronized metadata from Issue #${issueNumber}:\nLabels: ${issueLabels.join(', ')}\nMilestone: ${issue.milestone ? issue.milestone.title : 'None'}`
});

} catch (error) {
console.error(`Error updating PR #${prNumber}:`, error.message);
console.error(`Error syncing issue #${issueNumber} to PR #${prNumber}:`, error.message);
}
}
}
Loading