diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index f0696f23e..fc873058a 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -10,8 +10,6 @@ on: permissions: contents: read - pull-requests: write - packages: write jobs: static-checks: @@ -125,63 +123,163 @@ jobs: echo "outcome=success" >> $GITHUB_OUTPUT - - name: Create PR comment - if: always() - uses: actions/github-script@v6 - env: - HADOLINT_RESULT: ${{ steps.hadolint.outcome }} - CODE_LINT_RESULT: ${{ steps.code_lint.outcome }} - UNIT_TEST_RESULT: ${{ steps.unit_tests.outcome }} - DOCKLE_RESULT: ${{ steps.dockle.outcome }} - with: - script: | - const fs = require('fs'); - - const hadolintResult = fs.existsSync('${{ matrix.workdir }}/hadolint_output.json') ? fs.readFileSync('${{ matrix.workdir }}/hadolint_output.json', 'utf8') : 'No output'; - const codeLintResult = fs.existsSync('${{ matrix.workdir }}/code_lint_output.txt') ? fs.readFileSync('${{ matrix.workdir }}/code_lint_output.txt', 'utf8') : 'No output'; - const unitTestResult = fs.existsSync('${{ matrix.workdir }}/unit_test_output.txt') ? fs.readFileSync('${{ matrix.workdir }}/unit_test_output.txt', 'utf8') : 'No output'; - const dockleScanResult = fs.existsSync('${{ matrix.workdir }}/dockle_scan_output.json') ? fs.readFileSync('${{ matrix.workdir }}/dockle_scan_output.json', 'utf8') : 'No output'; - - let commentBody = ''; - - if (process.env.HADOLINT_RESULT !== 'success') { - commentBody = ` - :x: Dockerfile Lint (Hadolint) failed - \`\`\`json - ${hadolintResult} - \`\`\` - `; - } else if (process.env.CODE_LINT_RESULT !== 'success') { - commentBody = ` - :x: Code Lint failed - \`\`\` - ${codeLintResult} - \`\`\` - `; - } else if (process.env.UNIT_TEST_RESULT !== 'success') { - commentBody = ` - :x: Unit Tests failed - \`\`\` - ${unitTestResult} - \`\`\` - `; - } else if (process.env.DOCKLE_RESULT !== 'success') { - commentBody = ` - :x: Docker Image Scan (Dockle) failed - \`\`\`json - ${dockleScanResult} - \`\`\` - `; - } else { - commentBody = ':white_check_mark: All checks succeeded'; - } - - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: commentBody - }); + - name: Write test summary + if: ${{ always() }} + run: | + python3 - <<'PY' + import os + import sys + import json + from pathlib import Path + + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + sys.exit(0) + + workdir = "${{ matrix.workdir }}" + name = "${{ matrix.name }}" + + def md_escape(value): + return str(value).replace("|", "\\|").replace("\n", "
") + + def get_level_icon(level): + level = str(level).upper() + if level == "ERROR": + return "❌" + elif level == "WARN": + return "⚠️" + elif level == "INFO": + return "ℹ️" + else: + return "•" + + lines = [f"## {name} - Check Results", ""] + + # Hadolint Results + hadolint_file = Path(f"{workdir}/hadolint_output.json") + lines.extend([ + "### Dockerfile Lint (Hadolint)", + f"**Status:** ${{ steps.hadolint.outcome }}", + "" + ]) + if hadolint_file.exists(): + with open(hadolint_file) as f: + hadolint_data = json.load(f) + + if hadolint_data: + lines.extend([ + "| Code | Line | Level | Message |", + "| --- | ---: | --- | --- |", + ]) + for issue in hadolint_data: + code = md_escape(issue.get("code", "")) + line = issue.get("line", "") + level = issue.get("level", "") + icon = get_level_icon(level) + message = md_escape(issue.get("message", "")) + lines.append(f"| {code} | {line} | {icon} {level} | {message} |") + lines.append("") + else: + lines.extend(["✅ No issues found", ""]) + + # Code Lint Results + code_lint_file = Path(f"{workdir}/code_lint_output.txt") + lines.extend([ + "### Code Lint", + f"**Status:** ${{ steps.code_lint.outcome }}", + "" + ]) + if code_lint_file.exists(): + with open(code_lint_file) as f: + content = f.read().strip() + if content and content != "lint skipped": + lines.extend(["```", content, "```", ""]) + else: + lines.extend(["✅ Lint skipped or passed", ""]) + + # Unit Tests Results + unit_test_file = Path(f"{workdir}/unit_test_output.txt") + lines.extend([ + "### Unit Tests", + f"**Status:** ${{ steps.unit_tests.outcome }}", + "" + ]) + if unit_test_file.exists(): + with open(unit_test_file) as f: + content = f.read().strip() + if content and content != "unit tests skipped": + lines.extend(["```", content, "```", ""]) + else: + lines.extend(["✅ Unit tests skipped or passed", ""]) + + # Dockle Scan Results + dockle_file = Path(f"{workdir}/dockle_scan_output.json") + lines.extend([ + "### Docker Image Scan (Dockle)", + f"**Status:** ${{ steps.dockle.outcome }}", + "" + ]) + if dockle_file.exists(): + with open(dockle_file) as f: + dockle_data = json.load(f) + + summary = dockle_data.get("summary", {}) + fatal = summary.get('fatal', 0) + warn = summary.get('warn', 0) + info = summary.get('info', 0) + passed = summary.get('pass', 0) + + fatal_icon = "❌" if fatal >= 0 else "✅" + warn_icon = "⚠️" if warn > 0 else "✅" + info_icon = "ℹ️" if info > 0 else "✅" + + lines.extend([ + "#### Summary", + "", + "| Metric | Count |", + "| --- | ---: |", + f"| {fatal_icon} Fatal | {fatal} |", + f"| {warn_icon} Warn | {warn} |", + f"| {info_icon} Info | {info} |", + f"| ✅ Passed | {passed} |", + "", + ]) + + details = dockle_data.get("details", []) + if details: + lines.extend([ + "#### Details", + "", + "| Code | Title | Level | Alert |", + "| --- | --- | --- | --- |", + ]) + for detail in details: + code = md_escape(detail.get("code", "")) + title = md_escape(detail.get("title", "")) + level = detail.get("level", "") + icon = get_level_icon(level) + alerts = detail.get("alerts", []) + alert_text = md_escape(", ".join(alerts[:3])) # Limit to 3 alerts per row + if len(alerts) > 3: + alert_text += f", +{len(alerts)-3} more" + lines.append(f"| {code} | {title} | {icon} {level} | {alert_text} |") + lines.append("") + + # Metadata + lines.extend([ + "### Metadata", + "", + "| Key | Value |", + "| --- | --- |", + f"| Workdir | `{md_escape(workdir)}` |", + f"| Event | {md_escape(os.environ.get('GITHUB_EVENT_NAME', 'unknown'))} |", + f"| Ref | {md_escape(os.environ.get('GITHUB_REF_NAME', 'unknown'))} |", + f"| SHA | `{md_escape(os.environ.get('GITHUB_SHA', 'unknown')[:7])}` |", + f"| Actor | {md_escape(os.environ.get('GITHUB_ACTOR', 'unknown'))} |", + "", + ]) + + with open(summary_path, "a", encoding="utf-8") as summary: + summary.write("\n".join(lines)) + summary.write("\n") + PY