github: workflow: move things to separate alsa-project/github-workflows repo

It is a preparation to be reused for other ALSA repos.

Signed-off-by: Jaroslav Kysela <perex@perex.cz>
This commit is contained in:
Jaroslav Kysela 2026-04-16 15:03:33 +02:00
commit d10fcd2dca
5 changed files with 17 additions and 488 deletions

View file

@ -1,62 +0,0 @@
# 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@*'

View file

@ -1,5 +1,8 @@
# Example workflow for using the label commenter
Example workflow for using the label commenter
# Place this file in your repository at: .github/workflows/label-automation.yml
#
# You also need to create .github/label-descriptions.yml in your repository
# Example config: https://github.com/alsa-project/github-workflows/blob/main/repo/config/label-descriptions.yml
name: Label Automation
@ -13,11 +16,14 @@ on:
jobs:
handle-label:
uses: ./.github/workflows/reusable-label-commenter.yml
uses: alsa-project/github-workflows/repo/workflows/label-commenter.yml@main
permissions:
contents: read
pull-requests: write
issues: write
with:
config-path: '.github/label-descriptions.yml'
# Path to your local config file OR URL to remote config (required)
config-path: 'https://raw.githubusercontent.com/alsa-project/github-workflows/main/repo/config/label-descriptions.yml'
# Local file example: '.github/label-descriptions.yml'
# Remote URL example: 'https://raw.githubusercontent.com/alsa-project/github-workflows/main/repo/config/label-descriptions.yml'
secrets: inherit

View file

@ -1,5 +1,8 @@
# Example workflow for automatic Signed-off-by validation
# Place this file in your repository at: .github/workflows/pr-validation.yml
#
# You also need to create .github/label-descriptions.yml in your repository
# Example config: https://github.com/alsa-project/github-workflows/blob/main/repo/config/label-descriptions.yml
name: PR Validation
@ -17,13 +20,16 @@ on:
jobs:
validate-commits:
uses: ./.github/workflows/reusable-sob-validator.yml
uses: alsa-project/github-workflows/repo/workflows/sob-validator.yml@main
permissions:
contents: read
pull-requests: write
issues: write
with:
config-path: '.github/label-descriptions.yml'
# Path to your local config file OR URL to remote config (required)
config-path: 'https://raw.githubusercontent.com/alsa-project/github-workflows/main/repo/config/label-descriptions.yml'
# Local file example: '.github/label-descriptions.yml'
# Remote URL example: 'https://raw.githubusercontent.com/alsa-project/github-workflows/main/repo/config/label-descriptions.yml'
sob-label: 'signed off by'
pr-number: ${{ github.event_name == 'workflow_dispatch' && format('{0}', inputs.pr_number) || '' }}
secrets: inherit

View file

@ -1,126 +0,0 @@
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 }}

View file

@ -1,295 +0,0 @@
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}`);
}