--- /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'
+ secrets:
+ github-token:
+ description: 'GitHub token for API access'
+ required: false
+
+jobs:
+ add-label-comment:
+ runs-on: ubuntu-latest
+ 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 || 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: ''
+ 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: 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 || 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
+ const sobPattern = /^Signed-off-by:\s+(.+)\s+<(.+)>$/gm;
+ 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 || 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 || 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 || 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}`);
+ }