github: add GitHub label automation and SOB validation workflows #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Reusable Signed-off-by Validator | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| config-path: | ||
| description: 'Path to label descriptions config file' | ||
| required: false | ||
| type: string | ||
| default: '.github/label-descriptions.yml' | ||
| sob-label: | ||
| description: 'Label to add when SOB is missing or invalid' | ||
| required: false | ||
| type: string | ||
| default: 'signed off by' | ||
| secrets: | ||
| github-token: | ||
| description: 'GitHub token for API access' | ||
| required: false | ||
| jobs: | ||
| validate-signedoff: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Validate Signed-off-by | ||
| id: validate | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const fs = require('fs'); | ||
| const yaml = require('js-yaml'); | ||
| // Read config file | ||
| const configPath = '${{ inputs.config-path }}'; | ||
| let deniedEmails = []; | ||
| if (fs.existsSync(configPath)) { | ||
| const config = yaml.load(fs.readFileSync(configPath, 'utf8')); | ||
| deniedEmails = config.sob_validation?.denied_emails || []; | ||
| } | ||
| // Add default denied patterns | ||
| if (!deniedEmails.includes('*@users.noreply.github.com')) { | ||
| deniedEmails.push('*@users.noreply.github.com'); | ||
| } | ||
| const prNumber = context.payload.pull_request.number; | ||
| // Get all commits in the PR | ||
| const { data: commits } = await github.rest.pulls.listCommits({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: prNumber, | ||
| }); | ||
| const issues = []; | ||
| for (const commit of commits) { | ||
| const message = commit.commit.message; | ||
| const sha = commit.sha.substring(0, 7); | ||
| // Check for Signed-off-by line | ||
| const sobPattern = /^Signed-off-by:\s+(.+)\s+<(.+)>$/m; | ||
| const match = message.match(sobPattern); | ||
| if (!match) { | ||
| issues.push(`- Commit ${sha}: Missing Signed-off-by line`); | ||
| continue; | ||
| } | ||
| const email = match[2]; | ||
| // Check against denied email patterns | ||
| for (const pattern of deniedEmails) { | ||
| const regex = new RegExp('^' + pattern.replace('*', '.*') + '$'); | ||
| if (regex.test(email)) { | ||
| issues.push(`- Commit ${sha}: Invalid email in Signed-off-by: ${email}`); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| // Store results | ||
| core.setOutput('has_issues', issues.length > 0); | ||
| core.setOutput('issues', issues.join('\n')); | ||
| return issues.length > 0; | ||
| - name: Add label if issues found | ||
| if: steps.validate.outputs.has_issues == 'true' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.pull_request.number, | ||
| labels: ['${{ inputs.sob-label }}'], | ||
| }); | ||
| - name: Add comment with issues | ||
| if: steps.validate.outputs.has_issues == 'true' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const fs = require('fs'); | ||
| const yaml = require('js-yaml'); | ||
| const commentId = '<!-- sob-validator -->'; | ||
| const issues = process.env.SOB_ISSUES; | ||
| // Try to get custom message from config | ||
| let customMessage = ''; | ||
| const configPath = '${{ inputs.config-path }}'; | ||
| if (fs.existsSync(configPath)) { | ||
| const config = yaml.load(fs.readFileSync(configPath, 'utf8')); | ||
| const labelConfig = config.labels?.find(l => l.name === '${{ inputs.sob-label }}'); | ||
| if (labelConfig?.description) { | ||
| customMessage = '\n\n' + labelConfig.description; | ||
| } | ||
| } | ||
| const commentBody = `${commentId} | ||
| ## ⚠️ Signed-off-by Validation Issues | ||
| The following commits have issues with their Signed-off-by lines: | ||
| ${issues} | ||
| Please add a proper \`Signed-off-by: Your Name <your.email@example.com>\` line to each commit message.${customMessage}`; | ||
| // Check for existing comment | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.pull_request.number, | ||
| }); | ||
| const existingComment = comments.find(c => c.body?.includes(commentId)); | ||
| if (existingComment) { | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: existingComment.id, | ||
| body: commentBody, | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.pull_request.number, | ||
| body: commentBody, | ||
| }); | ||
| } | ||
| env: | ||
| SOB_ISSUES: ${{ steps.validate.outputs.issues }} | ||
| - name: Remove label if no issues | ||
| if: steps.validate.outputs.has_issues == 'false' | ||
| uses: actions/github-script@v7 | ||
| continue-on-error: true | ||
| with: | ||
| github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| try { | ||
| await github.rest.issues.removeLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.pull_request.number, | ||
| name: '${{ inputs.sob-label }}', | ||
| }); | ||
| } catch (error) { | ||
| // Label might not exist, that's fine | ||
| console.log('Label not present or already removed'); | ||
| } | ||