ci: combine header checks into workflow with PR comment #2
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Validate script headers | |
on: | |
push: | |
branches: | |
- main | |
pull_request: | |
paths: | |
- "ct/*.sh" | |
- "install/*.sh" | |
- ".github/workflows/validate-headers.yml" | |
jobs: | |
check-scripts: | |
name: Check changed files | |
runs-on: ubuntu-latest | |
permissions: | |
pull-requests: write | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }} | |
- name: Get changed files | |
id: changed-files | |
run: | | |
if ${{ github.event_name == 'pull_request' }}; then | |
echo "files=$(git diff --name-only -r HEAD^1 HEAD | grep -E '\.(sh|func)$' | xargs)" >> $GITHUB_OUTPUT | |
else | |
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | grep -E '\.(sh|func)$' | xargs)" >> $GITHUB_OUTPUT | |
fi | |
- name: Check build.func line | |
if: always() && steps.changed-files.outputs.files != '' | |
id: build-func | |
run: | | |
NON_COMPLIANT_FILES="" | |
for FILE in ${{ steps.changed-files.outputs.files }}; do | |
if [[ "$FILE" == ct/* ]] && [[ $(sed -n '2p' "$FILE") != "source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)" ]]; then | |
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE" | |
fi | |
done | |
if [ -n "$NON_COMPLIANT_FILES" ]; then | |
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT | |
echo "Build.func line missing or incorrect in files:" | |
for FILE in $NON_COMPLIANT_FILES; do | |
echo "$FILE" | |
done | |
exit 1 | |
fi | |
- name: Check executable permissions | |
if: always() && steps.changed-files.outputs.files != '' | |
id: check-executable | |
run: | | |
NON_COMPLIANT_FILES="" | |
for FILE in ${{ steps.changed-files.outputs.files }}; do | |
if [[ ! -x "$FILE" ]]; then | |
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE" | |
fi | |
done | |
if [ -n "$NON_COMPLIANT_FILES" ]; then | |
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT | |
echo "Files not executable:" | |
for FILE in $NON_COMPLIANT_FILES; do | |
echo "$FILE" | |
done | |
exit 1 | |
fi | |
- name: Check copyright | |
if: always() && steps.changed-files.outputs.files != '' | |
id: check-copyright | |
run: | | |
NON_COMPLIANT_FILES="" | |
for FILE in ${{ steps.changed-files.outputs.files }}; do | |
if [[ "$(sed -n '3p' "$FILE")" != "# Copyright (c) 2021-2025 community-scripts ORG" ]]; then | |
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE" | |
fi | |
done | |
if [ -n "$NON_COMPLIANT_FILES" ]; then | |
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT | |
echo "Copyright header missing or not on line 3 in files:" | |
for FILE in $NON_COMPLIANT_FILES; do | |
echo "$FILE" | |
done | |
exit 1 | |
fi | |
- name: Check author | |
if: always() && steps.changed-files.outputs.files != '' | |
id: check-author | |
run: | | |
NON_COMPLIANT_FILES="" | |
for FILE in ${{ steps.changed-files.outputs.files }}; do | |
if ! sed -n '4p' "$FILE" | grep -qE "^# Author: .+"; then | |
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE" | |
fi | |
done | |
if [ -n "$NON_COMPLIANT_FILES" ]; then | |
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT | |
echo "Author header missing or invalid on line 4 in files:" | |
for FILE in $NON_COMPLIANT_FILES; do | |
echo "$FILE" | |
done | |
exit 1 | |
fi | |
- name: Check license | |
if: always() && steps.changed-files.outputs.files != '' | |
id: check-license | |
run: | | |
NON_COMPLIANT_FILES="" | |
for FILE in ${{ steps.changed-files.outputs.files }}; do | |
if [[ "$(sed -n '5p' "$FILE")" != "# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE" ]]; then | |
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE" | |
fi | |
done | |
if [ -n "$NON_COMPLIANT_FILES" ]; then | |
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT | |
echo "License header missing or not on line 5 in files:" | |
for FILE in $NON_COMPLIANT_FILES; do | |
echo "$FILE" | |
done | |
exit 1 | |
fi | |
- name: Check source | |
if: always() && steps.changed-files.outputs.files != '' | |
id: check-source | |
run: | | |
NON_COMPLIANT_FILES="" | |
for FILE in ${{ steps.changed-files.outputs.files }}; do | |
if ! sed -n '6p' "$FILE" | grep -qE "^# Source: .+"; then | |
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE" | |
fi | |
done | |
if [ -n "$NON_COMPLIANT_FILES" ]; then | |
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT | |
echo "Source header missing or not on line 6 in files:" | |
for FILE in $NON_COMPLIANT_FILES; do | |
echo "$FILE" | |
done | |
exit 1 | |
fi | |
- name: Post results and comment | |
if: always() && steps.changed-files.outputs.files != '' && github.event_name == 'pull_request' | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const result = '${{ job.status }}' === 'success' ? 'success' : 'failure'; | |
const nonCompliantFiles = { | |
'Invalid build.func source': "${{ steps.build-func.outputs.files }}", | |
'Not executable': "${{ steps.check-executable.outputs.files }}", | |
'Copyright header line missing or invalid': "${{ steps.check-copyright.outputs.files }}", | |
'Author header line missing or invalid': "${{ steps.check-author.outputs.files }}", | |
'License header line missing or invalid': "${{ steps.check-license.outputs.files }}", | |
'Source header line missing or invalid': "${{ steps.check-source.outputs.files }}" | |
}; | |
const issueNumber = context.payload.pull_request ? context.payload.pull_request.number : null; | |
const commentIdentifier = 'validate-headers'; | |
let newCommentBody = `<!-- ${commentIdentifier}-start -->\n### Script header validation\n\n`; | |
if (result === 'failure') { | |
newCommentBody += ':x: We found issues in the following changed files:\n\n'; | |
for (const [check, files] of Object.entries(nonCompliantFiles)) { | |
if (files) { | |
newCommentBody += `**${check}:**\n${files.trim().split(' ').map(file => `- ${file}`).join('\n')}\n\n`; | |
} | |
} | |
} else { | |
newCommentBody += `:rocket: All changed shell scripts passed header validation!\n`; | |
} | |
newCommentBody += `\n\n<!-- ${commentIdentifier}-end -->`; | |
if (issueNumber) { | |
const { data: comments } = await github.rest.issues.listComments({ | |
...context.repo, | |
issue_number: issueNumber | |
}); | |
const existingComment = comments.find(comment => comment.user.login === 'github-actions[bot]'); | |
if (existingComment) { | |
if (existingComment.body.includes(commentIdentifier)) { | |
const re = new RegExp(String.raw`<!-- ${commentIdentifier}-start -->[\s\S]*?<!-- ${commentIdentifier}-end -->`, ""); | |
newCommentBody = existingComment.body.replace(re, newCommentBody); | |
} else { | |
newCommentBody = existingComment.body + '\n\n---\n\n' + newCommentBody; | |
} | |
await github.rest.issues.updateComment({ | |
...context.repo, | |
comment_id: existingComment.id, | |
body: newCommentBody | |
}); | |
} else { | |
await github.rest.issues.createComment({ | |
...context.repo, | |
issue_number: issueNumber, | |
body: newCommentBody | |
}); | |
} | |
} |