diff --git a/.github/workflows/contributor-onboarding.yml b/.github/workflows/contributor-onboarding.yml index 02a92ae..a8fba26 100644 --- a/.github/workflows/contributor-onboarding.yml +++ b/.github/workflows/contributor-onboarding.yml @@ -3,22 +3,21 @@ name: Contributor Onboarding on: pull_request_target: - types: [closed] + types: [opened, closed] branches: [main, master, develop, test] - issue_comment: - types: [created] permissions: pull-requests: write issues: write + contents: write jobs: - # Request Discord ID and wallet from first-time contributors + # Request Discord ID and wallet from contributors (on PR open) request-info: if: | - github.event_name == 'pull_request_target' && - github.event.pull_request.merged == true && - contains(github.event.pull_request.labels.*.name, 'first-time-contributor') + github.event_name == 'pull_request_target' && + github.event.action == 'opened' && + github.repository_owner == 'StabilityNexus' uses: StabilityNexus/ContributorAutomation/.github/workflows/reusable-request-info.yml@main with: pr_number: ${{ github.event.pull_request.number }} @@ -27,17 +26,37 @@ jobs: secrets: GIST_PAT: ${{ secrets.GIST_PAT }} - # Process contributor response with Discord ID and wallet + # Calculate lines changed for merged PRs + calculate-changes: + if: | + github.event_name == 'pull_request_target' && + github.event.action == 'closed' && + github.event.pull_request.merged == true && + github.repository_owner == 'StabilityNexus' + runs-on: ubuntu-latest + outputs: + lines_changed: ${{ steps.calc.outputs.lines_changed }} + steps: + - name: Calculate lines changed + id: calc + run: | + lines_changed=$(( ${{ github.event.pull_request.additions }} + ${{ github.event.pull_request.deletions }} )) + echo "lines_changed=${lines_changed}" >> "$GITHUB_OUTPUT" + + # Process contributor info from all PR comments (on PR merge) process-response: + needs: calculate-changes if: | - github.event_name == 'issue_comment' && - github.event.issue.pull_request != null && - (contains(github.event.comment.body, 'discord:') || contains(github.event.comment.body, 'Discord:')) + github.event_name == 'pull_request_target' && + github.event.action == 'closed' && + github.event.pull_request.merged == true && + github.repository_owner == 'StabilityNexus' uses: StabilityNexus/ContributorAutomation/.github/workflows/reusable-process-response.yml@main with: - pr_number: ${{ github.event.issue.number }} + pr_number: ${{ github.event.pull_request.number }} repo_name: ${{ github.repository }} - comment_body: ${{ github.event.comment.body }} - commenter: ${{ github.event.comment.user.login }} + pr_author: ${{ github.event.pull_request.user.login }} + pr_title: ${{ github.event.pull_request.title }} + lines_changed: ${{ needs.calculate-changes.outputs.lines_changed }} secrets: GIST_PAT: ${{ secrets.GIST_PAT }} diff --git a/.github/workflows/sync-pr-labels.yml b/.github/workflows/sync-pr-labels.yml new file mode 100644 index 0000000..a4aabc0 --- /dev/null +++ b/.github/workflows/sync-pr-labels.yml @@ -0,0 +1,343 @@ +name: Sync PR Labels + +on: + pull_request_target: + types: [opened, reopened, synchronize, edited] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + sync-labels: + if: ${{ github.repository_owner == 'StabilityNexus' }} + runs-on: ubuntu-latest + steps: + - name: Get PR details + id: pr-details + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + return { + number: pr.number, + body: pr.body || '', + base: pr.base.ref, + head: pr.head.ref + }; + + # STEP 1: Issue-based labels + - name: Extract linked issue number + id: extract-issue + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prBody = context.payload.pull_request.body || ''; + + // Match patterns: Fixes #123, Closes #123, Resolves #123, etc. + const issuePatterns = [ + /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+#(\d+)/gi, + /#(\d+)/g + ]; + + let issueNumber = null; + for (const pattern of issuePatterns) { + const match = prBody.match(pattern); + if (match) { + const numbers = match.map(m => m.match(/\d+/)[0]); + issueNumber = numbers[0]; + break; + } + } + + core.setOutput('issue_number', issueNumber || ''); + return issueNumber; + + - name: Apply issue-based labels + if: steps.extract-issue.outputs.issue_number != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueNumber = '${{ steps.extract-issue.outputs.issue_number }}'; + const prNumber = context.payload.pull_request.number; + + try { + // Fetch issue labels + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(issueNumber) + }); + + const issueLabels = issue.data.labels.map(label => + typeof label === 'string' ? label : label.name + ); + + if (issueLabels.length > 0) { + console.log(`Applying issue-based labels: ${issueLabels.join(', ')}`); + + // Add labels from issue + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: issueLabels + }); + } + } catch (error) { + console.log(`Error fetching issue #${issueNumber}: ${error.message}`); + } + + - name: Mark no issue linked + if: steps.extract-issue.outputs.issue_number == '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + console.log('No issue linked to this PR'); + + // Add "no-issue-linked" label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['no-issue-linked'] + }); + + # STEP 2: File-based labels + - name: Get changed files + id: changed-files + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + // Get list of files changed in the PR + const files = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const changedFiles = files.data.map(file => file.filename); + core.setOutput('files', JSON.stringify(changedFiles)); + + return changedFiles; + + - name: Apply file-based labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const changedFiles = JSON.parse('${{ steps.changed-files.outputs.files }}'); + + const fileLabels = []; + + // Define file-based label mappings + const labelMappings = { + 'documentation': ['.md', 'README', 'CONTRIBUTING', 'LICENSE', '.txt'], + 'frontend': ['.html', '.css', '.scss', '.jsx', '.tsx', '.vue'], + 'backend': ['.py', '.java', '.go', '.rb', '.php', '.rs'], + 'javascript': ['.js', '.ts', '.jsx', '.tsx'], + 'python': ['.py'], + 'configuration': ['.yml', '.yaml', '.json', '.toml', '.ini', '.env', '.config'], + 'github-actions': ['.github/workflows/'], + 'dependencies': ['package.json', 'requirements.txt', 'Gemfile', 'Cargo.toml', 'go.mod', 'pom.xml'], + 'tests': ['test/', '__tests__/', '.test.', '.spec.', '_test.'], + 'docker': ['Dockerfile', 'docker-compose', '.dockerignore'], + 'ci-cd': ['.github/', '.gitlab-ci', 'Jenkinsfile', '.circleci'] + }; + + // Check each file against label mappings + for (const file of changedFiles) { + for (const [label, patterns] of Object.entries(labelMappings)) { + for (const pattern of patterns) { + if (file.includes(pattern) || file.endsWith(pattern)) { + if (!fileLabels.includes(label)) { + fileLabels.push(label); + } + } + } + } + } + + if (fileLabels.length > 0) { + console.log(`Applying file-based labels: ${fileLabels.join(', ')}`); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: fileLabels + }); + } else { + console.log('No file-based labels matched'); + } + + # STEP 3: PR size labels + - name: Apply PR size label + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + // Get PR details to calculate size + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const additions = pr.data.additions; + const deletions = pr.data.deletions; + const totalChanges = additions + deletions; + + console.log(`PR has ${additions} additions and ${deletions} deletions (${totalChanges} total changes)`); + + // Determine size label based on total changes + let sizeLabel = ''; + if (totalChanges <= 10) { + sizeLabel = 'size/XS'; + } else if (totalChanges <= 50) { + sizeLabel = 'size/S'; + } else if (totalChanges <= 200) { + sizeLabel = 'size/M'; + } else if (totalChanges <= 500) { + sizeLabel = 'size/L'; + } else { + sizeLabel = 'size/XL'; + } + + console.log(`Calculated size label: ${sizeLabel}`); + + // Get current labels on the PR + const currentLabels = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const existingSizeLabels = currentLabels.data + .map(label => label.name) + .filter(name => name.startsWith('size/')); + + // Check if the size label needs to be changed + if (existingSizeLabels.length === 1 && existingSizeLabels[0] === sizeLabel) { + console.log(`Size label ${sizeLabel} is already correct, no changes needed`); + return; + } + + // Remove outdated size labels only if they differ + if (existingSizeLabels.length > 0) { + console.log(`Removing outdated size labels: ${existingSizeLabels.join(', ')}`); + for (const label of existingSizeLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: label + }); + } + } + + // Apply the new size label + console.log(`Applying new size label: ${sizeLabel}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [sizeLabel] + }); + + # STEP 4: Contributor-based labels(in this later we can add logic of team p as discussed on discord) + - name: Apply contributor-based labels + uses: actions/github-script@v7 + env: + LABELLER_TOKEN: ${{ secrets.EXTERNAL_LABELLER_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ env.LABELLER_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const prAuthor = context.payload.pull_request.user.login; + + try { + // Check if user is a first-time contributor + const commits = await github.rest.repos.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + author: prAuthor + }); + + const contributorLabels = []; + + // First check if maintainer + let isMaintainer = false; + try { + const permissionLevel = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: prAuthor + }); + + if (['admin', 'maintain'].includes(permissionLevel.data.permission)) { + contributorLabels.push('org-Member'); + isMaintainer = true; + } + } catch (error) { + console.log('Could not check collaborator status'); + } + + // If not maintainer, check contributor type + if (!isMaintainer) { + if (commits.data.length <= 1) { + contributorLabels.push('first-time-contributor'); + } else { + contributorLabels.push('repeat-contributor'); + } + } + + if (contributorLabels.length > 0) { + console.log(`Applying contributor-based labels: ${contributorLabels.join(', ')}`); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: contributorLabels + }); + } + } catch (error) { + console.log(`Error applying contributor labels: ${error.message}`); + } + + # Summary step + - name: Label sync summary + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + // Get current labels on PR + const pr = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const currentLabels = pr.data.labels.map(label => label.name); + console.log('='.repeat(50)); + console.log('PR Label Sync Complete'); + console.log('='.repeat(50)); + console.log(`Current labels on PR #${prNumber}:`); + console.log(currentLabels.join(', ') || 'No labels'); + console.log('='.repeat(50)); \ No newline at end of file diff --git a/.github/workflows/track-contributor-prs.yml b/.github/workflows/track-contributor-prs.yml index fc013b7..0fa057f 100644 --- a/.github/workflows/track-contributor-prs.yml +++ b/.github/workflows/track-contributor-prs.yml @@ -31,6 +31,6 @@ jobs: repo_name: ${{ github.repository }} pr_author: ${{ github.event.pull_request.user.login }} pr_title: ${{ github.event.pull_request.title }} - lines_changed: "${{ needs.calculate-changes.outputs.lines_changed }}" + lines_changed: ${{ needs.calculate-changes.outputs.lines_changed }} secrets: GIST_PAT: ${{ secrets.GIST_PAT }}