name: Validate scripts on: push: branches: - main pull_request: paths: - "ct/*.sh" - "install/*.sh" - ".github/workflows/validate-scripts.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" | grep -qE "^# Copyright \(c\) [0-9]{4}(-[0-9]{4})? (tteck \| community-scripts ORG|community-scripts ORG|tteck)$"; 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-scripts'; let newCommentBody = `\n### Script 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 validation!\n`; } newCommentBody += `\n\n`; 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`[\s\S]*?`, ""); 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 }); } }