Validate Kernel Commits - Post Comments (Secure) #5
This file contains hidden or 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 Kernel Commits - Post Comments (Secure) | |
| on: | |
| workflow_run: | |
| workflows: ["Validate Kernel Commits"] | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| inputs: | |
| run_id: | |
| description: "Workflow run ID to fetch artifacts from" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| pull-requests: write # Need write to post comments | |
| jobs: | |
| post-comments: | |
| runs-on: ubuntu-latest | |
| # Only run if the check workflow succeeded or failed (not skipped/cancelled) | |
| # For workflow_dispatch, always run (manual testing) | |
| if: | | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') | |
| steps: | |
| - name: Download check results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: check-results | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }} | |
| - name: Verify artifact integrity | |
| run: | | |
| if [ ! -f pr_metadata/checksums.txt ]; then | |
| echo "⚠️ Warning: No checksums file found, skipping integrity check" | |
| else | |
| cd pr_metadata | |
| if sha256sum -c checksums.txt --quiet; then | |
| echo "✅ Artifact integrity verified" | |
| else | |
| echo "❌ Artifact integrity check failed!" | |
| exit 1 | |
| fi | |
| cd .. | |
| fi | |
| - name: Read and validate PR metadata | |
| id: pr_metadata | |
| run: | | |
| if [ ! -f pr_metadata/pr_number.txt ]; then | |
| echo "❌ PR metadata not found - check workflow may have failed before saving metadata" | |
| exit 1 | |
| fi | |
| # Read values into variables (not step outputs yet - validate first!) | |
| PR_NUMBER=$(cat pr_metadata/pr_number.txt) | |
| REPOSITORY=$(cat pr_metadata/repository.txt) | |
| BASE_REF=$(cat pr_metadata/base_ref.txt) | |
| HEAD_SHA=$(cat pr_metadata/head_sha.txt) | |
| HEAD_REPO=$(cat pr_metadata/head_repo.txt) | |
| # === CRITICAL VALIDATION: Prevent command injection === | |
| # Validate PR number is actually a number | |
| if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then | |
| echo "❌ Security: Invalid PR number format: $PR_NUMBER" | |
| exit 1 | |
| fi | |
| # Validate PR number is reasonable (1 to 7 digits) | |
| if [ "$PR_NUMBER" -lt 1 ] || [ "$PR_NUMBER" -gt 9999999 ]; then | |
| echo "❌ Security: PR number out of range: $PR_NUMBER" | |
| exit 1 | |
| fi | |
| # Validate repository format (owner/repo) | |
| if ! [[ "$REPOSITORY" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then | |
| echo "❌ Security: Invalid repository format: $REPOSITORY" | |
| exit 1 | |
| fi | |
| # Validate repository name length | |
| if [ ${#REPOSITORY} -gt 100 ]; then | |
| echo "❌ Security: Repository name too long" | |
| exit 1 | |
| fi | |
| # Validate SHA is exactly 40 hex characters | |
| if ! [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "❌ Security: Invalid SHA format: $HEAD_SHA" | |
| exit 1 | |
| fi | |
| # Validate branch name (alphanumeric, dots, slashes, dashes, underscores, curly braces) | |
| if ! [[ "$BASE_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then | |
| echo "❌ Security: Invalid base branch name: $BASE_REF" | |
| exit 1 | |
| fi | |
| # Validate branch name length | |
| if [ ${#BASE_REF} -gt 255 ]; then | |
| echo "❌ Security: Branch name too long" | |
| exit 1 | |
| fi | |
| # Validate head repo format (can be empty for deleted forks) | |
| if [ -n "$HEAD_REPO" ] && ! [[ "$HEAD_REPO" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then | |
| echo "❌ Security: Invalid head repo format: $HEAD_REPO" | |
| exit 1 | |
| fi | |
| # === All validation passed - safe to use === | |
| echo "✅ All metadata validation passed" | |
| # Now safe to output (these will be used in subsequent steps) | |
| echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "repository=$REPOSITORY" >> $GITHUB_OUTPUT | |
| echo "base_ref=$BASE_REF" >> $GITHUB_OUTPUT | |
| echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT | |
| echo "head_repo=$HEAD_REPO" >> $GITHUB_OUTPUT | |
| - name: Check if PR is from fork | |
| id: is_fork | |
| run: | | |
| if [ "${{ steps.pr_metadata.outputs.head_repo }}" = "${{ steps.pr_metadata.outputs.repository }}" ]; then | |
| echo "result=false" >> $GITHUB_OUTPUT | |
| echo "✅ Internal PR detected" | |
| else | |
| echo "result=false" >> $GITHUB_OUTPUT | |
| echo "✅ Fork PR detected - JIRA checks will be skipped" | |
| fi | |
| - name: Validate comment files | |
| run: | | |
| # Validate result files exist and are reasonable size | |
| if [ -f ckc_result.txt ]; then | |
| FILE_SIZE=$(stat -f%z ckc_result.txt 2>/dev/null || stat -c%s ckc_result.txt 2>/dev/null) | |
| if [ "$FILE_SIZE" -gt 1048576 ]; then # 1MB limit | |
| echo "❌ Security: ckc_result.txt file too large: $FILE_SIZE bytes" | |
| exit 1 | |
| fi | |
| echo "✅ ckc_result.txt validated (size: $FILE_SIZE bytes)" | |
| fi | |
| if [ -f interdiff_result.txt ]; then | |
| FILE_SIZE=$(stat -f%z interdiff_result.txt 2>/dev/null || stat -c%s interdiff_result.txt 2>/dev/null) | |
| if [ "$FILE_SIZE" -gt 1048576 ]; then # 1MB limit | |
| echo "❌ Security: interdiff_result.txt file too large: $FILE_SIZE bytes" | |
| exit 1 | |
| fi | |
| echo "✅ interdiff_result.txt validated (size: $FILE_SIZE bytes)" | |
| fi | |
| - name: Comment on PR if check-kernel-commits issues found | |
| if: hashFiles('ckc_result.txt') != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} | |
| REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} | |
| run: | | |
| # Check if there are findings | |
| if grep -q "All referenced commits exist upstream and have no Fixes: tags." ckc_result.txt; then | |
| LINE_COUNT=$(wc -l < ckc_result.txt) | |
| if [ "$LINE_COUNT" -le 1 ]; then | |
| echo "✅ No check-kernel-commits findings, skipping comment" | |
| exit 0 | |
| fi | |
| fi | |
| # Post comment using environment variables (prevents injection) | |
| if ! gh pr comment "$PR_NUMBER" \ | |
| --body-file ckc_result.txt \ | |
| --repo "$REPOSITORY"; then | |
| echo "❌ Failed to post check-kernel-commits comment to PR" | |
| exit 1 | |
| fi | |
| echo "✅ Posted check-kernel-commits comment to PR #$PR_NUMBER" | |
| - name: Comment on PR if interdiff differences found | |
| if: hashFiles('interdiff_result.txt') != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} | |
| REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} | |
| run: | | |
| # Check if there are differences | |
| if grep -q "All backported commits match their upstream counterparts." interdiff_result.txt; then | |
| LINE_COUNT=$(wc -l < interdiff_result.txt) | |
| if [ "$LINE_COUNT" -le 1 ]; then | |
| echo "✅ No interdiff differences, skipping comment" | |
| exit 0 | |
| fi | |
| fi | |
| # Post comment using environment variables (prevents injection) | |
| if ! gh pr comment "$PR_NUMBER" \ | |
| --body-file interdiff_result.txt \ | |
| --repo "$REPOSITORY"; then | |
| echo "❌ Failed to post interdiff comment to PR" | |
| exit 1 | |
| fi | |
| echo "✅ Posted interdiff comment to PR #$PR_NUMBER" | |
| - name: Checkout PR head for JIRA check | |
| if: steps.is_fork.outputs.result == 'false' | |
| env: | |
| CLONE_URL: ${{ github.event_name == 'workflow_dispatch' && format('{0}/{1}.git', github.server_url, github.repository) || github.event.workflow_run.repository.clone_url }} | |
| BASE_REF: ${{ steps.pr_metadata.outputs.base_ref }} | |
| HEAD_SHA: ${{ steps.pr_metadata.outputs.head_sha }} | |
| run: | | |
| # Use environment variables to prevent command injection | |
| git clone --depth=1 --no-checkout "$CLONE_URL" -b "$BASE_REF" . | |
| git fetch --depth=100 origin "$HEAD_SHA" | |
| git checkout "$HEAD_SHA" | |
| # Verify we're on the expected commit | |
| ACTUAL_SHA=$(git rev-parse HEAD) | |
| if [ "$ACTUAL_SHA" != "$HEAD_SHA" ]; then | |
| echo "❌ Security: SHA mismatch after checkout!" | |
| echo "Expected: $HEAD_SHA" | |
| echo "Actual: $ACTUAL_SHA" | |
| exit 1 | |
| fi | |
| echo "✅ Checked out commit $HEAD_SHA" | |
| - name: Checkout kernel-src-tree-tools for JIRA check | |
| if: steps.is_fork.outputs.result == 'false' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ctrliq/kernel-src-tree-tools | |
| ref: 'mainline' | |
| path: kernel-src-tree-tools | |
| - name: Set up Python for JIRA check | |
| if: steps.is_fork.outputs.result == 'false' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.x' | |
| - name: Install JIRA PR Check dependencies | |
| if: steps.is_fork.outputs.result == 'false' | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install jira | |
| - name: Mask JIRA credentials | |
| if: steps.is_fork.outputs.result == 'false' | |
| run: | | |
| echo "::add-mask::${{ secrets.JIRA_API_TOKEN }}" | |
| echo "::add-mask::${{ secrets.JIRA_API_USER }}" | |
| echo "::add-mask::${{ secrets.JIRA_URL }}" | |
| - name: Run JIRA PR Check | |
| if: steps.is_fork.outputs.result == 'false' | |
| id: jira_check | |
| continue-on-error: true # Allow PR comments to be posted before failing workflow | |
| env: | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_API_USER: ${{ secrets.JIRA_API_USER }} | |
| JIRA_URL: ${{ secrets.JIRA_URL }} | |
| BASE_REF: ${{ steps.pr_metadata.outputs.base_ref }} | |
| HEAD_SHA: ${{ steps.pr_metadata.outputs.head_sha }} | |
| working-directory: kernel-src-tree-tools | |
| run: | | |
| # Run script and capture output, ensuring credentials are never echoed | |
| set +x # Disable command echo to prevent credential exposure | |
| set +e # Don't exit on error, we want to capture the output | |
| # Use environment variables for all arguments (prevents injection) | |
| OUTPUT=$(python3 jira_pr_check.py \ | |
| --kernel-src-tree .. \ | |
| --merge-target "$BASE_REF" \ | |
| --pr-branch "$HEAD_SHA" 2>&1) | |
| EXIT_CODE=$? | |
| # Filter out any potential credential leaks from output | |
| # Use fixed string matching to avoid regex issues with special chars in tokens | |
| FILTERED_OUTPUT=$(echo "$OUTPUT" | grep -v -F -e "jira-user" -e "jira-key" -e "basic_auth" -e "Authorization" -e "Bearer") | |
| # Additional safety: remove any lines that might contain the actual token | |
| # Do this without putting the token in the command line | |
| FILTERED_OUTPUT=$(echo "$FILTERED_OUTPUT" | grep -v "$JIRA_API_USER" || true) | |
| echo "$FILTERED_OUTPUT" | |
| # Save output using heredoc (safer than multiline strings) | |
| { | |
| echo "output<<JIRA_OUTPUT_EOF" | |
| echo "$FILTERED_OUTPUT" | |
| echo "JIRA_OUTPUT_EOF" | |
| } >> $GITHUB_OUTPUT | |
| # Check if there are any issues based on output patterns | |
| if echo "$FILTERED_OUTPUT" | grep -q "❌ Errors:"; then | |
| echo "has_issues=true" >> $GITHUB_OUTPUT | |
| # Check specifically for LTS mismatch errors | |
| if echo "$FILTERED_OUTPUT" | grep -q "expects branch"; then | |
| echo "has_lts_mismatch=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT | |
| fi | |
| elif echo "$FILTERED_OUTPUT" | grep -q "⚠️ Warnings:"; then | |
| echo "has_issues=true" >> $GITHUB_OUTPUT | |
| echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_issues=false" >> $GITHUB_OUTPUT | |
| echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Exit with the script's exit code | |
| exit $EXIT_CODE | |
| - name: Comment PR with JIRA issues | |
| if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outputs.has_issues == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} | |
| REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} | |
| JIRA_OUTPUT: ${{ steps.jira_check.outputs.output }} | |
| run: | | |
| # Validate output size before posting | |
| OUTPUT_SIZE=${#JIRA_OUTPUT} | |
| if [ "$OUTPUT_SIZE" -gt 65536 ]; then # 64KB limit | |
| echo "⚠️ JIRA output too large ($OUTPUT_SIZE bytes), truncating" | |
| JIRA_OUTPUT="${JIRA_OUTPUT:0:65000}... (output truncated due to size)" | |
| fi | |
| # Post comment using environment variable (prevents injection) | |
| if ! gh pr comment "$PR_NUMBER" \ | |
| --body "$JIRA_OUTPUT" \ | |
| --repo "$REPOSITORY"; then | |
| echo "❌ Failed to post JIRA check comment to PR" | |
| exit 1 | |
| fi | |
| echo "✅ Posted JIRA comment to PR #$PR_NUMBER" | |
| - name: Request changes if LTS mismatch | |
| if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outputs.has_lts_mismatch == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} | |
| REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} | |
| run: | | |
| gh pr review "$PR_NUMBER" \ | |
| --request-changes \ | |
| --body "⚠️ This PR contains VULN tickets that do not match the target LTS product. Please review the JIRA ticket assignments and ensure they match the merge target branch." \ | |
| --repo "$REPOSITORY" | |
| echo "✅ Requested changes on PR #$PR_NUMBER" | |
| - name: Fail workflow if JIRA errors found | |
| if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outcome == 'failure' | |
| run: | | |
| echo "❌ JIRA PR check failed - errors were found in one or more commits" | |
| exit 1 |