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:
parent
70d2463dc3
commit
d10fcd2dca
5 changed files with 17 additions and 488 deletions
62
.github/label-descriptions.yml
vendored
62
.github/label-descriptions.yml
vendored
|
|
@ -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@*'
|
||||
12
.github/workflows/label-automation.yml
vendored
12
.github/workflows/label-automation.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
.github/workflows/pr-validation.yml
vendored
10
.github/workflows/pr-validation.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
126
.github/workflows/reusable-label-commenter.yml
vendored
126
.github/workflows/reusable-label-commenter.yml
vendored
|
|
@ -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 }}
|
||||
295
.github/workflows/reusable-sob-validator.yml
vendored
295
.github/workflows/reusable-sob-validator.yml
vendored
|
|
@ -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}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue