diff --git a/.github/workflows/autolabel-pr-issue.yml b/.github/workflows/autolabel-pr-issue.yml new file mode 100644 index 00000000..00823215 --- /dev/null +++ b/.github/workflows/autolabel-pr-issue.yml @@ -0,0 +1,218 @@ +name: Auto Label PR from Linked Issue + +on: + pull_request_target: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: write + issues: write + contents: read + +jobs: + label-pr: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract Issue Numbers from PR Body + 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 }); + } + + 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 + 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 allLabels = new Set(); + + for (const issueNumber of issueNumbers) { + try { + const 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 + 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 + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: labels + }); + + console.log(`Successfully applied ${labels.length} labels to PR #${prNumber}`); + + // Add a comment to the 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(', ')}` + }); + } catch (error) { + console.error(`Error updating PR #${prNumber}:`, error.message); + } + } \ No newline at end of file diff --git a/.github/workflows/autolabler.yml b/.github/workflows/autolabler.yml index 36ea4bed..c93a9e50 100644 --- a/.github/workflows/autolabler.yml +++ b/.github/workflows/autolabler.yml @@ -24,10 +24,10 @@ jobs: await github.rest.issues.addLabels({ ...context.repo, issue_number: prNumber, - labels: ["recode", "level 1", "hacktoberfest-accepted"] + labels: ["recode","hacktoberfest-accepted"] }); - console.log(`Added labels [recode, level 1, hacktoberfest-accepted] to PR #${prNumber}`); + console.log(`Added labels [recode, hacktoberfest-accepted] to PR #${prNumber}`); - name: Add labels to Issue if: github.event_name == 'issues'