+++ /dev/null
-# Label descriptions configuration
-# Each label can have a description that will be posted as a comment when the label is applied
-
-labels:
- - name: kernel driver
- description: |
- ## ALSA Linux kernel driver work
-
- **Note:** This issue does not appear to be related to this repository, but rather seems to be an ALSA kernel driver issue.
-
- Please direct ALSA driver related discussions to the linux-sound@vger.kernel.org mailing list.
-
- **Bugtracker:** https://bugzilla.kernel.org (Audio component)
-
- For kernel patches, please see: https://www.kernel.org/doc/html/latest/process/submitting-patches.html
-
- - name: signed off by
- description: |
- ## Missing correct Signed-off-by line
-
- This pull request has commits that are missing proper `Signed-off-by` lines or use invalid email addresses.
-
- **Requirements:**
- - Each commit must include at least one `Signed-off-by: Your Name <your.email@example.com>` line
- - Commit author email must appear in at least one Signed-off-by line
- - Commit committer email must appear in at least one Signed-off-by line (if not a GitHub bot)
- - Multiple Signed-off-by lines are allowed for co-authored commits
- - Anonymous GitHub emails (like `@users.noreply.github.com`) are not allowed
- - The Signed-off-by line indicates that you agree to the Developer Certificate of Origin (DCO)
-
- **How to fix:**
- ```bash
- # Configure your git identity first (if needed):
- git config user.name "Your Name"
- git config user.email "your.email@example.com"
-
- # For the last commit:
- git commit --amend --signoff
-
- # For multiple commits, use interactive rebase:
- git rebase -i HEAD~N --signoff
-
- # For co-authored commits, add multiple Signed-off-by lines manually:
- git commit --amend
- # Then add in the commit message:
- # Signed-off-by: Author One <author1@example.com>
- # Signed-off-by: Author Two <author2@example.com>
-
- # Then force push:
- git push --force-with-lease
- ```
-
- **Reference:** [Developer Certificate of Origin](https://developercertificate.org/)
-
-# Signed-off-by validation configuration
-sob_validation:
- # Email patterns to deny (supports wildcards)
- denied_emails:
- - '*@users.noreply.github.com'
- # Add more patterns as needed:
- # - '*@example-blocked-domain.com'
- # - 'noreply@*'
+++ /dev/null
-name: Reusable Label Commenter
-
-on:
- workflow_call:
- inputs:
- config-path:
- description: 'Path to label descriptions config file'
- required: false
- type: string
- default: '.github/label-descriptions.yml'
-
-jobs:
- add-label-comment:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- pull-requests: write
- issues: write
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Read and parse config
- id: read-config
- run: |
- if [ ! -f "${{ inputs.config-path }}" ]; then
- echo "exists=false" >> $GITHUB_OUTPUT
- echo "Warning: Config file ${{ inputs.config-path }} not found"
- exit 0
- fi
-
- echo "exists=true" >> $GITHUB_OUTPUT
-
- # Convert YAML to JSON using Python
- python3 << 'PYTHON_EOF' > /tmp/config.json
- import yaml
- import json
- with open('${{ inputs.config-path }}', 'r') as f:
- config = yaml.safe_load(f)
- print(json.dumps(config))
- PYTHON_EOF
-
- # Store JSON as output (escaped for multiline)
- echo "config<<EOF" >> $GITHUB_OUTPUT
- cat /tmp/config.json >> $GITHUB_OUTPUT
- echo "EOF" >> $GITHUB_OUTPUT
-
- - name: Handle label action
- uses: actions/github-script@v7
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- // Get the label and action
- const label = context.payload.label.name;
- const action = context.payload.action;
-
- // Check if issue or PR
- const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
- if (!issueNumber) {
- console.log('No issue or PR number found');
- return;
- }
-
- // Create unique identifier for this label comment
- const commentId = `<!-- label-commenter:${label} -->`;
-
- // Get existing comments
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issueNumber,
- });
-
- // Find existing comment with this label ID
- const existingComment = comments.find(comment =>
- comment.body?.includes(commentId)
- );
-
- if (action === 'unlabeled') {
- // Label was removed - delete the comment if it exists
- if (existingComment) {
- await github.rest.issues.deleteComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existingComment.id,
- });
- console.log(`Deleted comment for removed label: ${label}`);
- } else {
- console.log(`No comment found for removed label: ${label}`);
- }
- } else if (action === 'labeled') {
- // Label was added - add or update comment
- const config = JSON.parse(process.env.CONFIG_JSON || '{}');
-
- // Find description for this label
- const labelConfig = config.labels?.find(l => l.name === label);
-
- if (!labelConfig || !labelConfig.description) {
- console.log(`No description configured for label: ${label}`);
- return;
- }
-
- const commentBody = `${commentId}\n${labelConfig.description}`;
-
- if (existingComment) {
- // Update existing comment
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existingComment.id,
- body: commentBody,
- });
- console.log(`Updated comment for label: ${label}`);
- } else {
- // Create new comment
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issueNumber,
- body: commentBody,
- });
- console.log(`Created comment for label: ${label}`);
- }
- }
- env:
- CONFIG_JSON: ${{ steps.read-config.outputs.config }}
+++ /dev/null
-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'
- pr-number:
- description: 'PR number to validate (optional, auto-detects from event if not provided)'
- required: false
- type: string
- default: ''
-
-jobs:
- validate-signedoff:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- pull-requests: write
- issues: write
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Determine PR number
- id: pr-number
- uses: actions/github-script@v7
- with:
- script: |
- // Get PR number from input (if manually triggered) or from event
- const inputPrNumber = '${{ inputs.pr-number }}';
- const prNumber = (inputPrNumber && inputPrNumber.trim() !== '')
- ? inputPrNumber
- : context.payload.pull_request?.number;
-
- if (!prNumber) {
- core.setFailed('No PR number available from input or event');
- return;
- }
- core.setOutput('number', prNumber);
- console.log(`Using PR number: ${prNumber}`);
-
- - name: Read and parse config
- id: read-config
- run: |
- if [ ! -f "${{ inputs.config-path }}" ]; then
- echo "exists=false" >> $GITHUB_OUTPUT
- echo "{}" > /tmp/config.json
- else
- echo "exists=true" >> $GITHUB_OUTPUT
- # Convert YAML to JSON using Python
- python3 << 'PYTHON_EOF' > /tmp/config.json
- import yaml
- import json
- with open('${{ inputs.config-path }}', 'r') as f:
- config = yaml.safe_load(f)
- print(json.dumps(config))
- PYTHON_EOF
- fi
-
- # Store JSON as output (escaped for multiline)
- echo "config<<EOF" >> $GITHUB_OUTPUT
- cat /tmp/config.json >> $GITHUB_OUTPUT
- echo "EOF" >> $GITHUB_OUTPUT
-
- - name: Validate Signed-off-by
- id: validate
- uses: actions/github-script@v7
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- // Parse config from JSON
- const config = JSON.parse(process.env.CONFIG_JSON);
- let 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 = ${{ steps.pr-number.outputs.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);
- const authorEmail = commit.commit.author.email;
- const committerEmail = commit.commit.committer.email;
-
- // Find all Signed-off-by lines (case-insensitive)
- const sobPattern = /^Signed-off-by:\s+(.+)\s+<(.+)>$/gim;
- const sobMatches = [...message.matchAll(sobPattern)];
-
- if (sobMatches.length === 0) {
- issues.push(`- Commit ${sha}: Missing Signed-off-by line`);
- continue;
- }
-
- // Extract all SOB emails
- const sobEmails = sobMatches.map(match => match[2]);
-
- // Check if author email is in at least one Signed-off-by line
- if (!sobEmails.includes(authorEmail)) {
- issues.push(`- Commit ${sha}: Commit author email (${authorEmail}) not found in any Signed-off-by line`);
- continue;
- }
-
- // Check if committer email is in at least one Signed-off-by line
- // (Skip check for GitHub web-flow and noreply bots which are common for web commits)
- const isGitHubBot = committerEmail.includes('noreply.github.com') ||
- committerEmail === 'noreply@github.com';
- if (!isGitHubBot && !sobEmails.includes(committerEmail)) {
- issues.push(`- Commit ${sha}: Commit committer email (${committerEmail}) not found in any Signed-off-by line`);
- continue;
- }
-
- // Check all SOB emails against denied patterns
- let deniedEmailFound = false;
- for (const sobEmail of sobEmails) {
- for (const pattern of deniedEmails) {
- const regex = new RegExp('^' + pattern.replace('*', '.*') + '$');
- if (regex.test(sobEmail)) {
- issues.push(`- Commit ${sha}: Invalid email in Signed-off-by: ${sobEmail}`);
- deniedEmailFound = true;
- break;
- }
- }
- if (deniedEmailFound) break;
- }
- if (deniedEmailFound) continue;
-
- // Check author email against denied patterns
- let authorDenied = false;
- for (const pattern of deniedEmails) {
- const regex = new RegExp('^' + pattern.replace('*', '.*') + '$');
- if (regex.test(authorEmail)) {
- issues.push(`- Commit ${sha}: Invalid commit author email: ${authorEmail}`);
- authorDenied = true;
- break;
- }
- }
- if (authorDenied) continue;
-
- // Check committer email against denied patterns (skip GitHub bots)
- if (!isGitHubBot) {
- for (const pattern of deniedEmails) {
- const regex = new RegExp('^' + pattern.replace('*', '.*') + '$');
- if (regex.test(committerEmail)) {
- issues.push(`- Commit ${sha}: Invalid commit committer email: ${committerEmail}`);
- break;
- }
- }
- }
- }
-
- // Store results
- core.setOutput('has_issues', issues.length > 0);
- core.setOutput('issues', issues.join('\n'));
-
- return issues.length > 0;
- env:
- CONFIG_JSON: ${{ steps.read-config.outputs.config }}
-
- - name: Add label if issues found
- if: steps.validate.outputs.has_issues == 'true'
- uses: actions/github-script@v7
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- await github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: ${{ steps.pr-number.outputs.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 }}
- script: |
- const commentId = '<!-- sob-validator -->';
- const issues = process.env.SOB_ISSUES;
-
- // Try to get custom message from config
- let customMessage = '';
- const config = JSON.parse(process.env.CONFIG_JSON);
- const labelConfig = config.labels?.find(l => l.name === '${{ inputs.sob-label }}');
- if (labelConfig?.description) {
- customMessage = '\n\n' + labelConfig.description;
- }
-
- const commentBody = commentId + '\n' +
- '## ⚠️ Signed-off-by Validation Issues\n\n' +
- 'The following commits have issues with their Signed-off-by lines:\n\n' +
- issues + '\n\n' +
- '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: ${{ steps.pr-number.outputs.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: ${{ steps.pr-number.outputs.number }},
- body: commentBody,
- });
- }
- env:
- SOB_ISSUES: ${{ steps.validate.outputs.issues }}
- CONFIG_JSON: ${{ steps.read-config.outputs.config }}
-
- - name: Remove label and comment 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 }}
- script: |
- const prNumber = parseInt('${{ steps.pr-number.outputs.number }}', 10);
- const labelName = '${{ inputs.sob-label }}';
-
- // Remove label
- try {
- await github.rest.issues.removeLabel({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: prNumber,
- name: labelName,
- });
- console.log(`Removed label: ${labelName}`);
- } catch (error) {
- if (error.status === 404) {
- console.log('Label not present or already removed');
- } else {
- console.log(`Error removing label: ${error.message}`);
- }
- }
-
- // Remove validation comment if it exists
- try {
- const commentId = '<!-- sob-validator -->';
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: prNumber,
- });
-
- const existingComment = comments.find(c => c.body?.includes(commentId));
-
- if (existingComment) {
- await github.rest.issues.deleteComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existingComment.id,
- });
- console.log('Removed validation comment');
- }
- } catch (error) {
- console.log(`Error removing comment: ${error.message}`);
- }