Skip to content

github: add GitHub label automation and SOB validation workflows #2

github: add GitHub label automation and SOB validation workflows

github: add GitHub label automation and SOB validation workflows #2

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}

Check failure on line 137 in .github/workflows/reusable-sob-validator.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/reusable-sob-validator.yml

Invalid workflow file

You have an error in your yaml syntax on line 137
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');
}