diff --git a/.github/.env.base b/.github/.env.base index 00dd75b..451d073 100644 --- a/.github/.env.base +++ b/.github/.env.base @@ -106,7 +106,7 @@ ENABLE_GODOCS_PUBLISHING=true # Publish to pkg.go.dev on tag/releases ARTIFACT_DOWNLOAD_RETRIES=3 # Number of retry attempts for failed downloads ARTIFACT_DOWNLOAD_RETRY_DELAY=10 # Initial retry delay in seconds (uses exponential backoff) ARTIFACT_DOWNLOAD_TIMEOUT=300 # Download timeout in seconds (5 minutes) -ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR=false # Continue workflow execution even if artifact download fails +ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR=true # Continue workflow execution even if artifact download fails (required for fork PRs) # ================================================================================================ # โš™๏ธ BENCHMARK & TEST CONFIGURATION @@ -299,12 +299,12 @@ NANCY_VERSION=v1.0.51 # https://github.com/sonatype-nexus-commu # ================================================================================================ # Pre-Commit System -GO_PRE_COMMIT_VERSION=v1.3.4 # https://github.com/mrz1836/go-pre-commit +GO_PRE_COMMIT_VERSION=v1.3.5 # https://github.com/mrz1836/go-pre-commit GO_PRE_COMMIT_USE_LOCAL=false # Use local version for development # System Settings GO_PRE_COMMIT_FAIL_FAST=false -GO_PRE_COMMIT_TIMEOUT_SECONDS=300 +GO_PRE_COMMIT_TIMEOUT_SECONDS=720 GO_PRE_COMMIT_TOOL_INSTALL_TIMEOUT=300 GO_PRE_COMMIT_AUTO_ADJUST_CI_TIMEOUTS=true GO_PRE_COMMIT_PARALLEL_WORKERS=2 @@ -347,7 +347,7 @@ GO_PRE_COMMIT_AI_DETECTION_AUTO_FIX=false GO_PRE_COMMIT_FMT_TIMEOUT=30 GO_PRE_COMMIT_FUMPT_TIMEOUT=30 GO_PRE_COMMIT_GOIMPORTS_TIMEOUT=30 -GO_PRE_COMMIT_LINT_TIMEOUT=60 +GO_PRE_COMMIT_LINT_TIMEOUT=600 GO_PRE_COMMIT_MOD_TIDY_TIMEOUT=60 GO_PRE_COMMIT_WHITESPACE_TIMEOUT=30 GO_PRE_COMMIT_EOF_TIMEOUT=30 @@ -409,6 +409,10 @@ AUTO_MERGE_COMMENT_ON_ENABLE=true AUTO_MERGE_COMMENT_ON_DISABLE=true AUTO_MERGE_LABELS_TO_ADD=automerge-enabled AUTO_MERGE_SKIP_BOT_PRS=true +AUTO_MERGE_SKIP_FORK_PRS=true +# Note: Fork PRs receive welcome comments from pull-request-management-fork.yml instead +# This setting only affects same-repo PRs (fork PRs use read-only GITHUB_TOKEN) +AUTO_MERGE_COMMENT_ON_FORK_SKIP=true # ================================================================================================ # ๐Ÿ“ PULL REQUEST MANAGEMENT CONFIGURATION diff --git a/.github/actions/load-env/action.yml b/.github/actions/load-env/action.yml index ce83565..fc0ed02 100644 --- a/.github/actions/load-env/action.yml +++ b/.github/actions/load-env/action.yml @@ -73,6 +73,50 @@ runs: fi } + # Function to validate environment variable names and values + validate_env_vars() { + local json="$1" + local source="$2" + + echo "๐Ÿ”’ Validating environment variables from $source..." + + # Extract all keys and values + local keys=$(echo "$json" | jq -r 'keys[]') + + while IFS= read -r key; do + # Skip empty keys + [[ -z "$key" ]] && continue + + # Validate key name: must match ^[A-Z_][A-Z0-9_]*$ + if ! echo "$key" | grep -qE '^[A-Z_][A-Z0-9_]*$'; then + echo "โŒ ERROR: Invalid environment variable name in $source: '$key'" >&2 + echo " Variable names must start with uppercase letter or underscore" >&2 + echo " and contain only uppercase letters, numbers, and underscores" >&2 + exit 1 + fi + + # Get the value for this key + local value=$(echo "$json" | jq -r --arg k "$key" '.[$k]') + + # Validate value length (max 10000 chars to prevent DoS) + if [[ ${#value} -gt 10000 ]]; then + echo "โŒ ERROR: Environment variable value too long in $source: '$key'" >&2 + echo " Maximum length is 10000 characters, got ${#value}" >&2 + exit 1 + fi + + # Check for suspicious command injection patterns + if echo "$value" | grep -qE '`|\$\(|\$\{|;|&|\||<\(|>|<|\\|'"'"'|"|\x00|[[:cntrl:]]'; then + echo "โš ๏ธ WARNING: Potentially unsafe characters in $source variable '$key'" >&2 + echo " Value contains backticks, command substitution, or shell metacharacters" >&2 + echo " Value will be treated as a literal string during extraction" >&2 + fi + + done <<< "$keys" + + echo "โœ… All variables in $source passed validation" + } + # Load configuration files in order of precedence BASE_JSON="{}" CUSTOM_JSON="{}" @@ -83,6 +127,9 @@ runs: BASE_JSON=$(parse_env_file ".github/.env.base") BASE_COUNT=$(echo "$BASE_JSON" | jq 'keys | length') echo "โœ… Loaded $BASE_COUNT base configuration variables" + + # Validate base configuration + validate_env_vars "$BASE_JSON" ".env.base" else echo "โŒ ERROR: Required .env.base file not found!" >&2 exit 1 @@ -94,6 +141,9 @@ runs: CUSTOM_JSON=$(parse_env_file ".github/.env.custom") CUSTOM_COUNT=$(echo "$CUSTOM_JSON" | jq 'keys | length') echo "โœ… Loaded $CUSTOM_COUNT custom override variables" + + # Validate custom configuration + validate_env_vars "$CUSTOM_JSON" ".env.custom" else echo "โ„น๏ธ No custom configuration file found (this is optional)" fi diff --git a/.github/labels.yml b/.github/labels.yml index 204a016..7c70e24 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -43,6 +43,9 @@ - name: "feature" description: "Any new significant addition" color: 0e8a16 +- name: "fork-pr" + description: "PR originated from a forked repository" + color: 5319e7 - name: "github-actions" description: "Used for referencing GitHub Actions" color: 006b75 diff --git a/.github/workflows/auto-merge-on-approval.yml b/.github/workflows/auto-merge-on-approval.yml index 2e1c48f..68daae5 100644 --- a/.github/workflows/auto-merge-on-approval.yml +++ b/.github/workflows/auto-merge-on-approval.yml @@ -100,7 +100,6 @@ jobs: id: config env: ENV_JSON: ${{ needs.load-env.outputs.env-json }} - GH_PAT_TOKEN: ${{ secrets.GH_PAT_TOKEN }} run: | echo "๐Ÿ“‹ Extracting auto-merge configuration from environment..." @@ -116,6 +115,8 @@ jobs: COMMENT_ON_DISABLE=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_COMMENT_ON_DISABLE') LABELS_TO_ADD=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_LABELS_TO_ADD') SKIP_BOT_PRS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_BOT_PRS') + SKIP_FORK_PRS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_FORK_PRS') + COMMENT_ON_FORK_SKIP=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_COMMENT_ON_FORK_SKIP') PREFERRED_TOKEN=$(echo "$ENV_JSON" | jq -r '.PREFERRED_GITHUB_TOKEN') # Validate required configuration @@ -135,6 +136,8 @@ jobs: echo "COMMENT_ON_DISABLE=$COMMENT_ON_DISABLE" >> $GITHUB_ENV echo "LABELS_TO_ADD=$LABELS_TO_ADD" >> $GITHUB_ENV echo "SKIP_BOT_PRS=$SKIP_BOT_PRS" >> $GITHUB_ENV + echo "SKIP_FORK_PRS=$SKIP_FORK_PRS" >> $GITHUB_ENV + echo "COMMENT_ON_FORK_SKIP=$COMMENT_ON_FORK_SKIP" >> $GITHUB_ENV # Determine default merge type DEFAULT_MERGE_TYPE=$(echo "$MERGE_TYPES" | cut -d',' -f1) @@ -156,12 +159,9 @@ jobs: echo " ๐Ÿ’ฌ Comment on disable: $COMMENT_ON_DISABLE" echo " ๐Ÿท๏ธ Labels to add: $LABELS_TO_ADD" echo " ๐Ÿค– Skip bot PRs: $SKIP_BOT_PRS" - - if [[ "$PREFERRED_TOKEN" == "GH_PAT_TOKEN" && -n "$GH_PAT_TOKEN" ]]; then - echo " ๐Ÿ”‘ Token: Personal Access Token (PAT)" - else - echo " ๐Ÿ”‘ Token: Default GITHUB_TOKEN" - fi + echo " ๐Ÿด Skip fork PRs: $SKIP_FORK_PRS" + echo " ๐Ÿ’ฌ Comment on fork skip: $COMMENT_ON_FORK_SKIP" + echo " ๐Ÿ”‘ Token: Selected via github-script action" # -------------------------------------------------------------------- # Process the PR for auto-merge @@ -198,6 +198,43 @@ jobs: return; } + // โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” + // Check if we should skip fork PRs + // โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” + + // Handle edge case: fork repository deleted/inaccessible (pr.head.repo is null) + if (!pr.head.repo) { + console.log('โš ๏ธ PR head repository is null (fork may have been deleted)'); + if (process.env.SKIP_FORK_PRS === 'true') { + console.log('๐Ÿด Skipping PR with deleted fork source (security policy)'); + core.setOutput('action', 'skip-deleted-fork'); + return; + } + // If not skipping forks, log and continue (will be treated as same-repo PR) + console.log('โš ๏ธ Continuing with auto-merge processing (null repo treated as same-repo)'); + } else { + // Safe to access pr.head.repo.full_name now + const headRepoFullName = pr.head.repo.full_name; + const baseRepoFullName = `${owner}/${repo}`; + const isForkPR = headRepoFullName !== baseRepoFullName; + + if (isForkPR && process.env.SKIP_FORK_PRS === 'true') { + console.log('๐Ÿด Skipping fork PR (security policy: fork PRs are not auto-merged)'); + console.log(` Fork source: ${headRepoFullName}`); + console.log(` Base repository: ${baseRepoFullName}`); + console.log(' Security reason: Fork PRs require manual maintainer review before merge'); + + // Note: Comments are not posted to fork PRs due to read-only GITHUB_TOKEN permissions + // Fork PR handling is already managed by pull-request-management-fork.yml workflow + if (process.env.COMMENT_ON_FORK_SKIP === 'true') { + console.log(' โ„น๏ธ Comment posting skipped for fork PR (handled by fork PR workflow)'); + } + + core.setOutput('action', 'skip-fork'); + return; + } + } + // โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” // Check basic PR conditions // โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” @@ -287,7 +324,7 @@ jobs: execSync(`gh pr merge --disable-auto "${pr.html_url}"`, { env: { ...process.env, - GH_TOKEN: '${{ secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }}' + GH_TOKEN: process.env.GITHUB_TOKEN }, stdio: 'inherit' }); @@ -304,8 +341,17 @@ jobs: } core.setOutput('action', 'disabled-changes-requested'); - } catch (error) { - console.log('โ„น๏ธ Could not disable auto-merge (may not have been enabled)'); + } catch (disableError) { + // Differentiate between "not enabled" and actual failures + if (disableError.message && ( + disableError.message.includes('not enabled') || + disableError.message.includes('auto-merge is not enabled') + )) { + console.log('โ„น๏ธ Auto-merge was not enabled, no action needed'); + } else { + console.error(`โŒ Failed to disable auto-merge: ${disableError.message}`); + // Don't fail workflow, but log the error properly + } } return; } @@ -342,15 +388,29 @@ jobs: console.log(`๐Ÿš€ Enabling auto-merge with command: ${mergeCommand}`); - execSync(mergeCommand, { - env: { - ...process.env, - GH_TOKEN: '${{ secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }}' - }, - stdio: 'inherit' - }); + try { + execSync(mergeCommand, { + env: { + ...process.env, + GH_TOKEN: process.env.GITHUB_TOKEN + }, + stdio: 'inherit' + }); - console.log('โœ… Auto-merge enabled! PR will merge when all status checks pass.'); + console.log('โœ… Auto-merge enabled! PR will merge when all status checks pass.'); + } catch (enableError) { + // Handle race condition: another workflow run may have enabled auto-merge + if (enableError.message && ( + enableError.message.includes('already enabled') || + enableError.message.includes('auto-merge is already enabled') + )) { + console.log('โ„น๏ธ Auto-merge already enabled by another workflow run'); + core.setOutput('action', 'already-enabled'); + return; + } + // Re-throw other errors to be caught by outer catch block + throw enableError; + } // Add comment if configured if (process.env.COMMENT_ON_ENABLE === 'true') { @@ -445,6 +505,12 @@ jobs: "skip-bot") ACTION_DESC="๐Ÿค– Skipped (bot PR)" ;; + "skip-fork") + ACTION_DESC="๐Ÿด Skipped (fork PR - security policy)" + ;; + "skip-deleted-fork") + ACTION_DESC="๐Ÿด Skipped (deleted fork PR)" + ;; "skip-draft") ACTION_DESC="๐Ÿ“ Skipped (draft PR)" ;; @@ -477,6 +543,7 @@ jobs: SKIP_DRAFT=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_DRAFT') SKIP_WIP=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_WIP') SKIP_BOT_PRS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_BOT_PRS') + SKIP_FORK_PRS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_FORK_PRS') echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY @@ -486,6 +553,7 @@ jobs: echo "| Skip draft PRs | $SKIP_DRAFT |" >> $GITHUB_STEP_SUMMARY echo "| Skip WIP PRs | $SKIP_WIP |" >> $GITHUB_STEP_SUMMARY echo "| Skip bot PRs | $SKIP_BOT_PRS |" >> $GITHUB_STEP_SUMMARY + echo "| Skip fork PRs | $SKIP_FORK_PRS |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "---" >> $GITHUB_STEP_SUMMARY echo "๐Ÿค– _Automated by GitHub Actions_" >> $GITHUB_STEP_SUMMARY @@ -509,6 +577,9 @@ jobs: disabled-changes-requested) echo "๐Ÿ›‘ Action: Auto-merge disabled due to changes requested" ;; + skip-fork) + echo "๐Ÿด Action: Skipped - Fork PR (security policy)" + ;; skip-*) echo "โญ๏ธ Action: Skipped - $ACTION" ;; diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 97724b4..0857ef2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5fe9434cd24fe243e33e7f3305f8a5b519b70280 # v4.31.1 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5fe9434cd24fe243e33e7f3305f8a5b519b70280 # v4.31.1 + uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # uses a compiled language - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5fe9434cd24fe243e33e7f3305f8a5b519b70280 # v4.31.1 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 diff --git a/.github/workflows/fortress-completion-finalize.yml b/.github/workflows/fortress-completion-finalize.yml index 426cc65..a85d71c 100644 --- a/.github/workflows/fortress-completion-finalize.yml +++ b/.github/workflows/fortress-completion-finalize.yml @@ -134,7 +134,7 @@ jobs: echo "| **Workflow** | ${{ github.workflow }} |" echo "| **Run Number** | ${{ github.run_number }} |" echo "| **Trigger** | ${{ github.event_name }} |" - echo "| **Source** | ${{ github.event.pull_request.head.repo.full_name == github.repository && 'Internal' || 'Fork' }} |" + echo "| **Source** | ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == github.repository && 'Internal' || 'Fork' }} |" echo "" echo "

" } > final-report.md @@ -204,6 +204,41 @@ jobs: echo "" >> final-report.md + # Add fork PR specific information if this is a fork PR + if [[ "${{ env.INPUT_is-fork-pr }}" == "true" ]]; then + { + echo "" + echo "## ๐Ÿ” Fork PR Security Status" + echo "" + echo "โš ๏ธ **This workflow ran on a FORK Pull Request**" + echo "" + echo "**Security Mode:** \`${{ env.INPUT_fork-security-mode }}\`" + echo "" + echo "### Jobs Status for Fork PR" + echo "**โœ… Jobs That Ran Successfully:**" + echo "- Setup & Configuration" + echo "- MAGE-X Testing" + echo "- Code Quality Checks" + echo "- Pre-Commit System" + echo "- $([ "${{ env.INPUT_benchmarks-result }}" != "skipped" ] && echo "Benchmarks" || echo "_(Benchmarks were skipped)_")" + echo "" + echo "**โ›” Jobs Skipped for Security:**" + echo "- **Security Scans** - Requires secrets (\`OSSI_TOKEN\`, \`OSSI_USERNAME\`, \`GITLEAKS_LICENSE\`)" + echo "- **Test Suite** - Requires \`CODECOV_TOKEN\` for coverage uploads" + echo "- **Release** - PRs cannot trigger releases (tags only)" + echo "" + echo "### Why Were Jobs Skipped?" + echo "Fork PRs have restricted access to repository secrets for security:" + echo "- โœ… Prevents credential theft from malicious fork PRs" + echo "- โœ… Protects external service tokens (OSSI, Codecov)" + echo "- โœ… Prevents unauthorized access through workflow modifications" + echo "" + echo "**Note for Fork Contributors:**" + echo "Repository maintainers will review your PR and can manually run security scans if needed." + echo "All code quality checks and tests that don't require secrets have already run successfully!" + } >> final-report.md + fi + # Add release-specific information if this was a tag push if [[ "${{ github.ref }}" == refs/tags/v* ]]; then { diff --git a/.github/workflows/fortress-completion-report.yml b/.github/workflows/fortress-completion-report.yml index cb248dd..8e61186 100644 --- a/.github/workflows/fortress-completion-report.yml +++ b/.github/workflows/fortress-completion-report.yml @@ -85,6 +85,16 @@ on: required: false type: string default: "unknown" + is-fork-pr: + description: "Whether this is a fork PR" + required: false + type: string + default: "false" + fork-security-mode: + description: "Security mode for fork PRs" + required: false + type: string + default: "full" # Security: Restrictive default permissions with job-level overrides for least privilege access permissions: diff --git a/.github/workflows/fortress-completion-statistics.yml b/.github/workflows/fortress-completion-statistics.yml index 4b856d5..c6c2559 100644 --- a/.github/workflows/fortress-completion-statistics.yml +++ b/.github/workflows/fortress-completion-statistics.yml @@ -279,12 +279,23 @@ jobs: # Store metrics for output echo "cache-metrics={\"hit_rate\":$CACHE_HIT_RATE,\"total_hits\":$TOTAL_CACHE_HITS,\"total_attempts\":$TOTAL_CACHE_ATTEMPTS}" >> $GITHUB_OUTPUT fi - fi - # Add spacing after cache section - if compgen -G "cache-stats-*.json" >/dev/null 2>&1; then + # Add spacing after cache section echo "" >> statistics-section.md echo "

" >> statistics-section.md + else + # No cache statistics available + { + echo "" + echo "### ๐Ÿ’พ Cache Statistics" + echo "" + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Cache Data** | โš ๏ธ No cache statistics available |" + echo "| **Reason** | Cache stats may not be available for this workflow run |" + echo "" + echo "

" + } >> statistics-section.md fi # -------------------------------------------------------------------- @@ -365,6 +376,20 @@ jobs: # Store metrics for output echo "benchmark-metrics={\"total_benchmarks\":$TOTAL_BENCHMARKS,\"total_duration\":$TOTAL_DURATION,\"mode\":\"$BENCH_MODE\"}" >> $GITHUB_OUTPUT + else + # No benchmark statistics available + { + echo "" + echo "" + echo "### โšก Benchmark Results" + echo "" + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Benchmarks** | โš ๏ธ No benchmark data available |" + echo "| **Reason** | Benchmarks may have been skipped or data not uploaded |" + echo "" + echo "

" + } >> statistics-section.md fi # -------------------------------------------------------------------- diff --git a/.github/workflows/fortress-completion-tests.yml b/.github/workflows/fortress-completion-tests.yml index da9bf30..4a5c7d1 100644 --- a/.github/workflows/fortress-completion-tests.yml +++ b/.github/workflows/fortress-completion-tests.yml @@ -332,6 +332,21 @@ jobs: # Store failure metrics echo "failure-metrics={\"total_failures\":$TOTAL_FAILURES,\"has_error_output\":$HAS_ERROR_OUTPUT}" >> $GITHUB_OUTPUT fi + else + # No test statistics available - likely fork PR with skipped test suite + { + echo "" + echo "" + echo "### ๐Ÿงช Test Results Summary" + echo "" + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Test Suite** | โš ๏ธ Skipped - No test statistics available |" + echo "| **Reason** | Tests may have been skipped for fork PR security restrictions |" + echo "| **Note** | Repository maintainers can run full tests on merged code |" + echo "" + echo "_For security reasons, fork PRs do not have access to test execution secrets._" + } >> tests-section.md fi # -------------------------------------------------------------------- @@ -383,6 +398,10 @@ jobs: echo "- Estimated output size reduction: ~80-90% for large test suites" >> tests-section.md fi fi + else + # No test configuration to display - test stats not available + echo "" >> tests-section.md + echo "โ„น๏ธ _Test configuration section skipped - no test data available_" >> tests-section.md fi # -------------------------------------------------------------------- diff --git a/.github/workflows/fortress-security-scans.yml b/.github/workflows/fortress-security-scans.yml index 0c16437..abb117c 100644 --- a/.github/workflows/fortress-security-scans.yml +++ b/.github/workflows/fortress-security-scans.yml @@ -367,7 +367,7 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_HEAD_REF: ${{ github.head_ref }} - PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} run: | echo "๐Ÿ” Checking repository security conditions..." echo "Event Name: $GITHUB_EVENT_NAME" @@ -425,7 +425,7 @@ jobs: echo "| ๐Ÿ”’ Security Details | โš ๏ธ Status |" >> $GITHUB_STEP_SUMMARY echo "|---|---|" >> $GITHUB_STEP_SUMMARY echo "| **Tool** | Gitleaks |" >> $GITHUB_STEP_SUMMARY - echo "| **Fork Detected** | ${{ github.event.pull_request.head.repo.full_name || 'N/A (not a PR event)' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Fork Detected** | ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || 'N/A (not a PR event)' }} |" >> $GITHUB_STEP_SUMMARY echo "| **Base Repository** | ${{ github.repository }} |" >> $GITHUB_STEP_SUMMARY echo "| **Result** | โš ๏ธ Skipped for security (fork cannot access secrets) |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/fortress-setup-config.yml b/.github/workflows/fortress-setup-config.yml index 8423089..53b713b 100644 --- a/.github/workflows/fortress-setup-config.yml +++ b/.github/workflows/fortress-setup-config.yml @@ -167,6 +167,12 @@ on: redis-service-mode: description: "Redis service mode (auto, always, never)" value: ${{ jobs.setup-config.outputs.redis-service-mode }} + is-fork-pr: + description: "Whether this is a fork PR (true/false)" + value: ${{ jobs.setup-config.outputs.is-fork-pr }} + fork-security-mode: + description: "Security mode for fork PRs (safe/unsafe)" + value: ${{ jobs.setup-config.outputs.fork-security-mode }} # Security: Restrictive default permissions with job-level overrides for least privilege access permissions: contents: read @@ -217,6 +223,8 @@ jobs: redis-cache-force-pull: ${{ steps.redis-config.outputs.redis-cache-force-pull }} redis-trust-service-health: ${{ steps.redis-config.outputs.redis-trust-service-health }} redis-service-mode: ${{ steps.redis-config.outputs.redis-service-mode }} + is-fork-pr: ${{ steps.fork-detection.outputs.is-fork-pr }} + fork-security-mode: ${{ steps.fork-detection.outputs.fork-security-mode }} steps: # -------------------------------------------------------------------- # Start timer to record workflow start time @@ -230,6 +238,46 @@ jobs: echo "start-epoch=$START_EPOCH" >> $GITHUB_OUTPUT echo "๐Ÿš€ Workflow started at: $START_TIME" # -------------------------------------------------------------------- + # Detect Fork PR Status + # -------------------------------------------------------------------- + - name: ๐Ÿ” Detect Fork PR Status + id: fork-detection + env: + EVENT_NAME: ${{ github.event_name }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} + run: | + echo "๐Ÿ” Detecting fork status..." + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo "Event: $EVENT_NAME" + echo "PR Head Repo: $PR_HEAD_REPO" + echo "Base Repo: $BASE_REPO" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + + # Check if this is a fork PR + if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_REPO" && "$PR_HEAD_REPO" != "$BASE_REPO" ]]; then + echo "๐Ÿšจ FORK PR DETECTED" + echo "is-fork-pr=true" >> $GITHUB_OUTPUT + echo "fork-security-mode=safe" >> $GITHUB_OUTPUT + + echo "" + echo "โš ๏ธ Security Mode: SAFE (Fork PR)" + echo " - Security scans requiring secrets will be skipped" + echo " - Test suite with Codecov will be skipped" + echo " - Release job will be skipped (PRs can't trigger releases anyway)" + echo " - All other checks will run normally" + else + echo "โœ… NOT A FORK PR (Same repository or not a PR event)" + echo "is-fork-pr=false" >> $GITHUB_OUTPUT + echo "fork-security-mode=full" >> $GITHUB_OUTPUT + + echo "" + echo "โœ… Security Mode: FULL" + echo " - All jobs will run with full access to secrets" + fi + + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + # -------------------------------------------------------------------- # Parse environment variables from JSON # -------------------------------------------------------------------- - name: ๐Ÿ”ง Parse environment variables @@ -525,6 +573,38 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "

" >> $GITHUB_STEP_SUMMARY + # Fork PR Status (if applicable) + if [[ "${{ steps.fork-detection.outputs.is-fork-pr }}" == "true" ]]; then + echo "## ๐Ÿ” Fork PR Security Status" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "โš ๏ธ **This is a FORK Pull Request**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Security Mode:** \`${{ steps.fork-detection.outputs.fork-security-mode }}\` (restricted for security)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Jobs That Will Run:" >> $GITHUB_STEP_SUMMARY + echo "- โœ… **Setup & Configuration** - Environment detection and matrix generation" >> $GITHUB_STEP_SUMMARY + echo "- โœ… **MAGE-X Testing** - Build system verification" >> $GITHUB_STEP_SUMMARY + echo "- โœ… **Cache Warming** - Dependency and build cache preparation" >> $GITHUB_STEP_SUMMARY + echo "- โœ… **Code Quality** - golangci-lint, static analysis, YAML validation" >> $GITHUB_STEP_SUMMARY + echo "- โœ… **Pre-Commit Checks** - Formatting, whitespace, EOF checks (17x faster)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… **Benchmarks** - Performance testing and regression detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Jobs That Will Be Skipped (Require Secrets):" >> $GITHUB_STEP_SUMMARY + echo "- โ›” **Security Scans** - Nancy (requires \`OSSI_TOKEN\`), Govulncheck, Gitleaks" >> $GITHUB_STEP_SUMMARY + echo "- โ›” **Test Suite with Coverage** - Codecov upload (requires \`CODECOV_TOKEN\`)" >> $GITHUB_STEP_SUMMARY + echo "- โ›” **Release** - Already skipped for PRs (only runs on tags)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Why Are Some Jobs Skipped?" >> $GITHUB_STEP_SUMMARY + echo "Fork PRs run with limited access to repository secrets for security. This prevents:" >> $GITHUB_STEP_SUMMARY + echo "- Unauthorized access to external service credentials (OSSI, Codecov)" >> $GITHUB_STEP_SUMMARY + echo "- Potential credential theft from malicious fork PRs" >> $GITHUB_STEP_SUMMARY + echo "- Exposure of sensitive tokens through workflow modifications" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Maintainers will review your PR and can manually run security scans if needed.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "

" >> $GITHUB_STEP_SUMMARY + fi + # Configuration Statistics (moved up for overview) echo "## ๐Ÿ“ˆ Configuration Overview" >> $GITHUB_STEP_SUMMARY ENABLED_FEATURES=$(echo "$ENV_JSON" | jq -r '[to_entries | .[] | select(.key | startswith("ENABLE_")) | select(.value == "true")] | length') diff --git a/.github/workflows/fortress.yml b/.github/workflows/fortress.yml index 89c30ae..542b179 100644 --- a/.github/workflows/fortress.yml +++ b/.github/workflows/fortress.yml @@ -26,6 +26,21 @@ # This file is licensed under the MIT License. # Attribution is requested if reused: Created by @mrz1836 # +# FORK PR HANDLING: +# This workflow intelligently handles fork PRs by detecting fork status during setup +# and conditionally skipping jobs that require repository secrets. Jobs are categorized: +# +# FORK-SAFE (Always run - no secrets required): +# โœ… setup, test-magex, warm-cache, code-quality, pre-commit, benchmarks, status-check +# +# FORK-UNSAFE (Skipped on fork PRs - require secrets): +# โ›” security (OSSI_TOKEN, OSSI_USERNAME, GITLEAKS_LICENSE) +# โ›” test-suite (CODECOV_TOKEN for coverage uploads) +# โ›” release (already tag-only, but extra safety for forks) +# +# Fork contributors see clear messaging in setup summary explaining which jobs run. +# This provides security without workflow duplication or maintenance overhead. +# # ------------------------------------------------------------------------------------ name: GoFortress @@ -123,7 +138,7 @@ jobs: env-json: ${{ needs.load-env.outputs.env-json }} primary-runner: ${{ needs.setup.outputs.primary-runner }} # ---------------------------------------------------------------------------------- - # Warm Go Caches + # Warm Go Caches (FORK-SAFE: No secrets required) # ---------------------------------------------------------------------------------- warm-cache: name: ๐Ÿ’พ Warm Cache @@ -142,7 +157,7 @@ jobs: redis-cache-force-pull: ${{ needs.setup.outputs.redis-cache-force-pull }} go-sum-file: ${{ needs.setup.outputs.go-sum-file }} # ---------------------------------------------------------------------------------- - # Security Scans + # Security Scans (FORK-UNSAFE: Requires secrets - skipped on fork PRs) # ---------------------------------------------------------------------------------- security: name: ๐Ÿ”’ Security Scans @@ -152,7 +167,8 @@ jobs: needs.setup.result == 'success' && needs.test-magex.result == 'success' && (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') && - needs.setup.outputs.security-scans-enabled == 'true' + needs.setup.outputs.security-scans-enabled == 'true' && + needs.setup.outputs.is-fork-pr != 'true' permissions: contents: read # Read repository content for security scanning uses: ./.github/workflows/fortress-security-scans.yml @@ -170,7 +186,7 @@ jobs: ossi-token: ${{ secrets.OSSI_TOKEN }} ossi-username: ${{ secrets.OSSI_USERNAME }} # ---------------------------------------------------------------------------------- - # Code Quality Checks + # Code Quality Checks (FORK-SAFE: No secrets required) # ---------------------------------------------------------------------------------- code-quality: name: ๐Ÿ“Š Code Quality @@ -194,7 +210,7 @@ jobs: secrets: github-token: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} # ---------------------------------------------------------------------------------- - # Pre-commit Checks + # Pre-commit Checks (FORK-SAFE: No secrets required) # ---------------------------------------------------------------------------------- pre-commit: name: ๐Ÿช Pre-commit Checks @@ -215,7 +231,7 @@ jobs: pre-commit-enabled: ${{ needs.setup.outputs.pre-commit-enabled }} go-sum-file: ${{ needs.setup.outputs.go-sum-file }} # ---------------------------------------------------------------------------------- - # Test Suite + # Test Suite (FORK-UNSAFE: Requires CODECOV_TOKEN for coverage - skipped on fork PRs) # ---------------------------------------------------------------------------------- test-suite: name: ๐Ÿงช Test Suite @@ -224,7 +240,8 @@ jobs: !cancelled() && needs.setup.result == 'success' && needs.test-magex.result == 'success' && - (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') + (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') && + needs.setup.outputs.is-fork-pr != 'true' permissions: contents: write # Write repository content and push to gh-pages branch for test execution pull-requests: write # Required: Coverage workflow needs to create PR comments @@ -256,7 +273,7 @@ jobs: github-token: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # ---------------------------------------------------------------------------------- - # Benchmark Suite + # Benchmark Suite (FORK-SAFE: No secrets required) # ---------------------------------------------------------------------------------- benchmarks: name: ๐Ÿƒ Benchmarks @@ -383,13 +400,15 @@ jobs: echo "๐ŸŽ‰ All required checks passed (skipped jobs are considered OK)." # ---------------------------------------------------------------------------------- - # Release Version + # Release Version (FORK-UNSAFE: PRs never trigger this, but extra fork safety included) # ---------------------------------------------------------------------------------- release: name: ๐Ÿš€ Release Version - needs: [load-env, setup, test-suite, security, code-quality, pre-commit] - # Only run on successful tag pushes - if: startsWith(github.ref, 'refs/tags/v') + needs: [load-env, setup, test-magex, test-suite, security, code-quality, pre-commit] + # Only run on successful tag pushes from same repository (not forks) + if: | + startsWith(github.ref, 'refs/tags/v') && + needs.setup.outputs.is-fork-pr != 'true' uses: ./.github/workflows/fortress-release.yml with: env-json: ${{ needs.load-env.outputs.env-json }} @@ -407,7 +426,7 @@ jobs: # ---------------------------------------------------------------------------------- completion-report: name: ๐Ÿ“Š Workflow Completion Report - if: always() + if: always() && !contains(fromJSON('["failure", "cancelled"]'), needs.setup.result) && !contains(fromJSON('["failure", "cancelled"]'), needs.test-magex.result) needs: [load-env, setup, test-magex, pre-commit, security, code-quality, test-suite, benchmarks, release, status-check] permissions: contents: read # Read repository content for completion report @@ -430,3 +449,5 @@ jobs: test-suite-result: ${{ needs.test-suite.result }} gofortress-version: ${{ needs.setup.outputs.gofortress-version }} gofortress-released: ${{ needs.setup.outputs.gofortress-released }} + is-fork-pr: ${{ needs.setup.outputs.is-fork-pr }} + fork-security-mode: ${{ needs.setup.outputs.fork-security-mode }} diff --git a/.github/workflows/pull-request-management-fork.yml b/.github/workflows/pull-request-management-fork.yml new file mode 100644 index 0000000..fc3d24b --- /dev/null +++ b/.github/workflows/pull-request-management-fork.yml @@ -0,0 +1,445 @@ +# ------------------------------------------------------------------------------------ +# Pull Request Management for Forks Workflow +# +# Purpose: Automate labeling, assignment, and welcoming of pull requests for forked PRs. +# +# Configuration: All settings are loaded from .env.base and .env.custom files for +# centralized management across all workflows. +# +# Triggers: Pull request events (opened, reopened, ready for review, closed, synchronize) +# +# Features: +# - Automatic labeling based on branch prefix and PR title +# - Default assignee management +# - Welcome messages for first-time contributors +# - PR size analysis and labeling +# - Cache cleanup on PR close +# - Branch deletion after merge +# +# Maintainer: @mrz1836 +# +# SECURITY MODEL: +# - Uses pull_request_target trigger for write permissions (required for labels/comments) +# - CRITICAL: Only checks out BASE branch code, NEVER PR head (prevents malicious code execution) +# - Fork detection uses full_name comparison for accuracy (not owner.login which fails for org members) +# - All code execution happens from trusted base repository +# - No secrets exposed to fork PRs (GITHUB_TOKEN only) +# +# ------------------------------------------------------------------------------------ + +name: PR Management (Forks) + +# -------------------------------------------------------------------- +# Trigger Configuration +# -------------------------------------------------------------------- +on: + pull_request_target: + types: [opened, reopened, ready_for_review, closed, synchronize] + +# Least privilege at the workflow level; jobs get bumps as needed +permissions: + contents: read + +# -------------------------------------------------------------------- +# Concurrency Control +# -------------------------------------------------------------------- +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }}-fork + cancel-in-progress: true + +# -------------------------------------------------------------------- +# Environment Variables +# -------------------------------------------------------------------- +# Note: Configuration variables are loaded from .env.base and .env.custom files + +jobs: + # ------------------------------------------------------------ + # Load env from the BASE repo only (safe) for centralized config + # ------------------------------------------------------------ + load-env: + name: ๐ŸŒ Load Environment (Base Repo) + runs-on: ubuntu-latest + # No write perms here + permissions: + contents: read + outputs: + env-json: ${{ steps.load-env.outputs.env-json }} + steps: + - name: ๐Ÿ“ฅ Checkout base repo (sparse) + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # CRITICAL SECURITY: Always checkout base branch (not PR head) + # This prevents malicious code execution from fork PRs + # pull_request_target runs with write permissions, so we MUST NOT + # execute any code from the untrusted PR + ref: ${{ github.base_ref }} + fetch-depth: 1 + sparse-checkout: | + .github/.env.base + .github/.env.custom + .github/actions/load-env + + - name: ๐ŸŒ Load environment variables + id: load-env + uses: ./.github/actions/load-env + + # ------------------------------------------------------------ + # Detect if this is truly a fork PR with proper null handling + # ------------------------------------------------------------ + detect-fork: + name: ๐Ÿ” Detect Fork PR + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + is-fork: ${{ steps.detection.outputs.is-fork }} + steps: + - name: ๐Ÿ” Fork detection with null checks + id: detection + env: + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} + run: | + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo "๐Ÿ” Fork Detection Debug" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo " PR Head Repo: '${PR_HEAD_REPO}'" + echo " Base Repo: '${BASE_REPO}'" + echo " Event: ${{ github.event_name }}" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + + # Check if this is a fork PR with proper null/empty handling + # A fork PR is when: + # 1. PR_HEAD_REPO is not empty (not null/undefined) + # 2. PR_HEAD_REPO != BASE_REPO (different repositories) + if [[ -n "$PR_HEAD_REPO" ]] && [[ "$PR_HEAD_REPO" != "$BASE_REPO" ]]; then + echo "๐Ÿšจ FORK PR DETECTED" + echo "is-fork=true" >> $GITHUB_OUTPUT + else + echo "โœ… NOT A FORK PR (Same repository or invalid head repo)" + echo "is-fork=false" >> $GITHUB_OUTPUT + fi + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + + # ------------------------------------------------------------ + # Fork detector + labeller/commenter/assignee + # ------------------------------------------------------------ + handle-fork: + name: ๐Ÿท๏ธ Label/Assign/Comment (Fork PR) + needs: [load-env, detect-fork] + runs-on: ubuntu-latest + # Only run for fork PRs (different repository) - using detection output + if: needs.detect-fork.outputs.is-fork == 'true' + permissions: + # We need to WRITE to PR for labels/comments/assignees + pull-requests: write + issues: write + contents: read + steps: + - name: ๐Ÿ”ง Extract config + id: cfg + env: + ENV_JSON: ${{ needs.load-env.outputs.env-json }} + run: | + # pull minimal config, with sensible fallbacks + DEFAULT_ASSIGNEE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE // ""') + SKIP_BOT_USERS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SKIP_BOT_USERS // ""') + FORK_LABEL=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_FORK_LABEL // "fork-pr"') + TRIAGE_LABEL=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_TRIAGE_LABEL // "requires-manual-review"') + WELCOME_FORKS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FORKS // "true"') + + echo "DEFAULT_ASSIGNEE=$DEFAULT_ASSIGNEE" >> "$GITHUB_ENV" + echo "SKIP_BOT_USERS=$SKIP_BOT_USERS" >> "$GITHUB_ENV" + echo "FORK_LABEL=$FORK_LABEL" >> "$GITHUB_ENV" + echo "TRIAGE_LABEL=$TRIAGE_LABEL" >> "$GITHUB_ENV" + echo "WELCOME_FORKS=$WELCOME_FORKS" >> "$GITHUB_ENV" + + - name: ๐Ÿท๏ธ Add fork + triage labels + id: labels + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const prNumber = pr.number; + const author = pr.user.login; + + // Skip bots if configured + const skip = (process.env.SKIP_BOT_USERS || '') + .split(',').map(s => s.trim()).filter(Boolean); + if (skip.includes(author)) { + core.info(`Skipping labels for bot user: ${author}`); + return; + } + + const ensureLabels = async (names) => { + // create missing labels lazily (safe colors) + for (const name of names) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, repo: context.repo.repo, name + }); + } catch (e) { + if (e.status === 404) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name, + color: name === process.env.TRIAGE_LABEL ? "d876e3" : "ededed", + }); + core.info(`Created missing label: ${name}`); + } else { + throw e; + } + } + } + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [process.env.FORK_LABEL, process.env.TRIAGE_LABEL] + }); + }; + + await ensureLabels([process.env.FORK_LABEL, process.env.TRIAGE_LABEL]); + + - name: ๐Ÿ‘ค Assign default assignee (optional) + id: assign + if: env.DEFAULT_ASSIGNEE != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const author = pr.user.login; + + const skip = (process.env.SKIP_BOT_USERS || '') + .split(',').map(s => s.trim()).filter(Boolean); + if (skip.includes(author)) { + core.info(`Skipping assignment for bot user: ${author}`); + return; + } + + if ((pr.assignees || []).length === 0) { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + assignees: [process.env.DEFAULT_ASSIGNEE], + }); + core.info(`Assigned to @${process.env.DEFAULT_ASSIGNEE}`); + } else { + core.info('PR already has assignees; skipping.'); + } + + - name: ๐Ÿ’ฌ Comment notice for fork PR + id: comment + if: env.WELCOME_FORKS == 'true' && github.event.action == 'opened' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const author = pr.user.login; + const repoName = context.repo.repo; + const repoOwner = context.repo.owner; + + const body = `## ๐Ÿ‘‹ Thanks, @${author}! + + This pull request comes from a **fork**. For security, our CI runs in a restricted mode. + A maintainer will triage this shortly and run any additional checks as needed. + + - ๐Ÿท๏ธ Labeled: \`${process.env.FORK_LABEL}\`, \`${process.env.TRIAGE_LABEL}\` + - ๐Ÿ‘€ We'll review and follow up here if anything else is needed. + + Thanks for contributing to **${repoOwner}/${repoName}**! ๐Ÿš€ + + `; + + // Check for existing welcome comment to avoid duplicates + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100 + }); + + const welcomeExists = comments.some(comment => + comment.body.includes('') && + comment.user.login === 'github-actions[bot]' + ); + + if (!welcomeExists) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + core.info(`โœ… Posted welcome comment for fork PR from @${author}`); + } else { + core.info(`โ„น๏ธ Welcome comment already exists, skipping duplicate`); + } + + # ------------------------------------------------------------ + # Clean Runner Cache (on PR close) + # ------------------------------------------------------------ + clean-cache: + name: ๐Ÿงน Clean Runner Cache + needs: [load-env, detect-fork] + runs-on: ubuntu-latest + permissions: + actions: write # Required: Delete GitHub Actions caches for closed PRs + contents: read # Read repository content for cache management + if: github.event.action == 'closed' && needs.detect-fork.outputs.is-fork == 'true' + outputs: + caches-cleaned: ${{ steps.clean.outputs.caches-cleaned }} + + steps: + # -------------------------------------------------------------------- + # Extract configuration from env-json + # -------------------------------------------------------------------- + - name: ๐Ÿ”ง Extract configuration + id: config + env: + ENV_JSON: ${{ needs.load-env.outputs.env-json }} + run: | + echo "๐Ÿ“‹ Extracting PR management configuration from environment..." + + # Extract all needed variables + CLEAN_CACHE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_CLEAN_CACHE_ON_CLOSE // "true"') + + # Set as environment variables for all subsequent steps + echo "CLEAN_CACHE=$CLEAN_CACHE" >> $GITHUB_ENV + + # Log configuration + echo "๐Ÿ” Configuration loaded:" + echo " ๐Ÿงน Clean cache on close: $CLEAN_CACHE" + + # -------------------------------------------------------------------- + # Clean up caches associated with the PR + # -------------------------------------------------------------------- + - name: ๐Ÿงน Cleanup caches + id: clean + if: env.CLEAN_CACHE == 'true' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + echo "๐Ÿงน Cleaning up caches for fork PR #$PR_NUMBER..." + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + + # Fetch the list of cache keys for this PR + echo "๐Ÿ“‹ Fetching cache list for PR #$PR_NUMBER..." + + # Get all caches and filter for this PR (checking multiple possible refs) + allCaches=$(gh cache list --limit 100 --json id,key,ref) + + # Debug: Show what refs we're looking for + echo "๐Ÿ” Looking for caches with refs:" + echo " - refs/pull/$PR_NUMBER/merge" + echo " - refs/pull/$PR_NUMBER/head" + echo " - refs/heads/$PR_HEAD_REF" + + # Filter caches that belong to this PR (multiple possible refs) + cacheKeysForPR=$(echo "$allCaches" | jq -r --arg pr "$PR_NUMBER" --arg branch "$PR_HEAD_REF" \ + '.[] | select( + .ref == "refs/pull/\($pr)/merge" or + .ref == "refs/pull/\($pr)/head" or + .ref == "refs/heads/\($branch)" + ) | .id') + + # Count caches - handle empty results properly + if [ -z "$cacheKeysForPR" ]; then + cacheCount=0 + else + cacheCount=$(echo "$cacheKeysForPR" | wc -l | tr -d ' ') + fi + + if [ "$cacheCount" -eq "0" ]; then + echo "โ„น๏ธ No caches found for this PR" + echo "caches-cleaned=0" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "๐Ÿ—‘๏ธ Found $cacheCount cache(s) to clean" + + # Setting this to not fail the workflow while deleting cache keys + set +e + cleanedCount=0 + + # Delete each cache + for cacheKey in $cacheKeysForPR; do + if gh cache delete "$cacheKey"; then + echo " โœ… Deleted cache: $cacheKey" + ((cleanedCount++)) + else + echo " โš ๏ธ Failed to delete cache: $cacheKey" + fi + done + + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo "โœ… Cleaned $cleanedCount out of $cacheCount cache(s)" + echo "caches-cleaned=$cleanedCount" >> $GITHUB_OUTPUT + + # ------------------------------------------------------------ + # Human-friendly run summary + # ------------------------------------------------------------ + summary: + name: ๐Ÿ“Š Summary + runs-on: ubuntu-latest + if: always() + needs: [load-env, detect-fork, handle-fork, clean-cache] + steps: + - name: ๐Ÿ“„ Write summary + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_ACTION: ${{ github.event.action }} + IS_FORK: ${{ needs.detect-fork.outputs.is-fork }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} + run: | + echo "# ๐Ÿ”ง Fork PR Management Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**PR:** #$PR_NUMBER โ€” $PR_TITLE" >> $GITHUB_STEP_SUMMARY + echo "**Author:** @$PR_AUTHOR" >> $GITHUB_STEP_SUMMARY + echo "**Action:** $PR_ACTION" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Show fork detection results + echo "## ๐Ÿ” Fork Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| PR Head Repo | \`$PR_HEAD_REPO\` |" >> $GITHUB_STEP_SUMMARY + echo "| Base Repo | \`$BASE_REPO\` |" >> $GITHUB_STEP_SUMMARY + echo "| Is Fork PR? | **$IS_FORK** |" >> $GITHUB_STEP_SUMMARY + + if [ "$IS_FORK" = "true" ]; then + echo "| Status | โœ… Fork PR - Handled with restricted permissions |" >> $GITHUB_STEP_SUMMARY + else + echo "| Status | โ„น๏ธ **NOT a fork PR** - This workflow should not have processed this PR |" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Show cache cleanup results if PR was closed + if [ "$PR_ACTION" = "closed" ]; then + echo "## ๐Ÿงน Cleanup Actions" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Action | Result |" >> $GITHUB_STEP_SUMMARY + echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY + + # Cache cleanup + if [ "${{ needs.clean-cache.result }}" = "success" ]; then + CACHES="${{ needs.clean-cache.outputs.caches-cleaned }}" + echo "| ๐Ÿงน Cache Cleanup | $CACHES cache(s) cleaned |" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo "---" >> $GITHUB_STEP_SUMMARY + echo "**Security:** This workflow used **pull_request_target** and did **not** check out or execute the PR's code." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/pull-request-management.yml b/.github/workflows/pull-request-management.yml index 5fe9107..6ba09c8 100644 --- a/.github/workflows/pull-request-management.yml +++ b/.github/workflows/pull-request-management.yml @@ -8,7 +8,7 @@ # Configuration: All settings are loaded from .env.base and .env.custom files for # centralized management across all workflows. # -# Triggers: Pull request events (opened, reopened, ready for review, closed) +# Triggers: Pull request events (opened, reopened, ready for review, closed, synchronize) # # Features: # - Automatic labeling based on branch prefix and PR title @@ -20,6 +20,13 @@ # # Maintainer: @mrz1836 # +# SECURITY MODEL: +# - Uses pull_request trigger (runs in PR context with limited permissions) +# - Safe to check out PR code as workflow has read-only access by default +# - Fork detection uses full_name comparison for accuracy (not owner.login which fails for org members) +# - Job-level permissions grant write access only where needed (labels, comments, cache cleanup) +# - Mutually exclusive with fork workflow - each PR triggers only ONE workflow +# # ------------------------------------------------------------------------------------ name: PR Management @@ -29,7 +36,7 @@ name: PR Management # -------------------------------------------------------------------- on: pull_request: - types: [opened, reopened, ready_for_review, closed] + types: [opened, reopened, ready_for_review, closed, synchronize] # Security: Restrictive default permissions with job-level overrides for least privilege access permissions: @@ -75,17 +82,58 @@ jobs: uses: ./.github/actions/load-env id: load-env + # ---------------------------------------------------------------------------------- + # Detect if this is a same-repository PR with proper null handling + # ---------------------------------------------------------------------------------- + detect-same-repo: + name: ๐Ÿ” Detect Same-Repo PR + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + is-same-repo: ${{ steps.detection.outputs.is-same-repo }} + steps: + - name: ๐Ÿ” Same-repo detection with null checks + id: detection + env: + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} + run: | + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo "๐Ÿ” Same-Repo PR Detection Debug" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo " PR Head Repo: '${PR_HEAD_REPO}'" + echo " Base Repo: '${BASE_REPO}'" + echo " Event: ${{ github.event_name }}" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + + # Check if this is a same-repo PR with proper null/empty handling + # A same-repo PR is when: + # 1. PR_HEAD_REPO is not empty (not null/undefined) + # 2. PR_HEAD_REPO == BASE_REPO (same repository) + if [[ -n "$PR_HEAD_REPO" ]] && [[ "$PR_HEAD_REPO" == "$BASE_REPO" ]]; then + echo "โœ… SAME-REPO PR DETECTED" + echo "is-same-repo=true" >> $GITHUB_OUTPUT + else + echo "๐Ÿšจ NOT A SAME-REPO PR (Fork or invalid head repo)" + echo "is-same-repo=false" >> $GITHUB_OUTPUT + fi + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + # ---------------------------------------------------------------------------------- # Apply Labels Based on Branch and Title # ---------------------------------------------------------------------------------- apply-labels: name: ๐Ÿท๏ธ Apply Labels - needs: [load-env] + needs: [load-env, detect-same-repo] runs-on: ubuntu-latest permissions: contents: read pull-requests: write - if: github.event.action != 'closed' + # Only run for non-fork PRs (same repository) - using detection output + if: | + github.event.action != 'closed' && + needs.detect-same-repo.outputs.is-same-repo == 'true' outputs: labels-applied: ${{ steps.apply-labels.outputs.labels-applied }} @@ -248,14 +296,15 @@ jobs: # ---------------------------------------------------------------------------------- assign-assignee: name: ๐Ÿ‘ค Assign Default Assignee - needs: [load-env] + needs: [load-env, detect-same-repo] runs-on: ubuntu-latest permissions: contents: read pull-requests: write + # Only run for non-fork PRs (same repository) - using detection output if: | github.event.action != 'closed' && - github.event.pull_request.head.repo.owner.login == github.repository_owner + needs.detect-same-repo.outputs.is-same-repo == 'true' outputs: assignee-added: ${{ steps.assign.outputs.assignee-added }} @@ -333,7 +382,7 @@ jobs: # ---------------------------------------------------------------------------------- welcome-contributor: name: ๐Ÿ‘‹ Welcome New Contributor - needs: [load-env] + needs: [load-env, detect-same-repo] runs-on: ubuntu-latest permissions: contents: read @@ -341,7 +390,7 @@ jobs: if: | github.event.action == 'opened' && contains(fromJSON('["FIRST_TIMER", "FIRST_TIME_CONTRIBUTOR"]'), github.event.pull_request.author_association) && - github.event.pull_request.head.repo.owner.login == github.repository_owner + needs.detect-same-repo.outputs.is-same-repo == 'true' outputs: welcomed: ${{ steps.welcome.outputs.welcomed }} @@ -426,12 +475,14 @@ jobs: # ---------------------------------------------------------------------------------- analyze-size: name: ๐Ÿ“ Analyze PR Size - needs: [load-env] + needs: [load-env, detect-same-repo] runs-on: ubuntu-latest permissions: contents: read pull-requests: write - if: github.event.action == 'opened' + if: | + github.event.action == 'opened' && + needs.detect-same-repo.outputs.is-same-repo == 'true' outputs: size-label: ${{ steps.analyze.outputs.size-label }} total-changes: ${{ steps.analyze.outputs.total-changes }} @@ -530,7 +581,7 @@ jobs: # ---------------------------------------------------------------------------------- clean-cache: name: ๐Ÿงน Clean Runner Cache - needs: [load-env] + needs: [load-env, detect-same-repo] runs-on: ubuntu-latest permissions: actions: write # Required: Delete GitHub Actions caches for closed PRs @@ -633,14 +684,15 @@ jobs: # ---------------------------------------------------------------------------------- delete-branch: name: ๐ŸŒฟ Delete Merged Branch - needs: [load-env] + needs: [load-env, detect-same-repo] runs-on: ubuntu-latest permissions: contents: write # Required: Delete branches after PR merge + # Only run for non-fork PRs (same repository) that were merged - using detection output if: | github.event.action == 'closed' && github.event.pull_request.merged == true && - github.event.pull_request.head.repo.full_name == github.repository + needs.detect-same-repo.outputs.is-same-repo == 'true' outputs: branch-deleted: ${{ steps.delete.outputs.branch-deleted }} @@ -732,7 +784,7 @@ jobs: summary: name: ๐Ÿ“Š Generate Summary if: always() - needs: [load-env, apply-labels, assign-assignee, welcome-contributor, analyze-size, clean-cache, delete-branch] + needs: [load-env, detect-same-repo, apply-labels, assign-assignee, welcome-contributor, analyze-size, clean-cache, delete-branch] runs-on: ubuntu-latest steps: # -------------------------------------------------------------------- @@ -746,6 +798,9 @@ jobs: PR_ACTION: ${{ github.event.action }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_MERGED: ${{ github.event.pull_request.merged }} + IS_SAME_REPO: ${{ needs.detect-same-repo.outputs.is-same-repo }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} run: | echo "๐Ÿ“Š Generating workflow summary..." @@ -755,9 +810,64 @@ jobs: echo "**๐Ÿ“‹ PR:** #$PR_NUMBER - $PR_TITLE" >> $GITHUB_STEP_SUMMARY echo "**๐ŸŽฌ Action:** $PR_ACTION" >> $GITHUB_STEP_SUMMARY echo "**๐Ÿ‘ค Author:** @$PR_AUTHOR" >> $GITHUB_STEP_SUMMARY - echo "**๐Ÿ”— Source:** ${{ github.event.pull_request.head.repo.full_name == github.repository && 'Internal' || 'Fork' }}" >> $GITHUB_STEP_SUMMARY + + # Show repo detection results + echo "" >> $GITHUB_STEP_SUMMARY + echo "## ๐Ÿ” Repository Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| PR Head Repo | \`$PR_HEAD_REPO\` |" >> $GITHUB_STEP_SUMMARY + echo "| Base Repo | \`$BASE_REPO\` |" >> $GITHUB_STEP_SUMMARY + echo "| Is Same Repo? | **$IS_SAME_REPO** |" >> $GITHUB_STEP_SUMMARY + + if [ "$IS_SAME_REPO" = "true" ]; then + echo "| Status | โœ… Same-repo PR - Full automation enabled |" >> $GITHUB_STEP_SUMMARY + else + echo "| Status | โš ๏ธ **NOT a same-repo PR** - This workflow should not have processed this PR |" >> $GITHUB_STEP_SUMMARY + fi echo "" >> $GITHUB_STEP_SUMMARY + # Add fork PR specific information if this is a fork PR + if [ "$IS_SAME_REPO" = "false" ]; then + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## ๐Ÿ” Fork PR Status" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "โš ๏ธ **This is a FORK Pull Request** - Some automated actions are restricted for security." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$PR_ACTION" != "closed" ]; then + echo "### โœ… Actions Completed for Fork PR:" >> $GITHUB_STEP_SUMMARY + echo "- **Labels Applied** - Automated based on branch prefix and PR title" >> $GITHUB_STEP_SUMMARY + echo "- **Type Detection** - PR type classification" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โ›” Actions Skipped (Fork Restrictions):" >> $GITHUB_STEP_SUMMARY + echo "- **Default Assignee** - Fork PRs are not auto-assigned" >> $GITHUB_STEP_SUMMARY + echo "- **Size Analysis** - Only available for internal PRs" >> $GITHUB_STEP_SUMMARY + echo "- **Branch Operations** - Fork branches cannot be deleted from base repo" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ“ Why Are Actions Restricted?" >> $GITHUB_STEP_SUMMARY + echo "Fork PRs have limited permissions to protect repository security:" >> $GITHUB_STEP_SUMMARY + echo "- Prevents unauthorized repository modifications" >> $GITHUB_STEP_SUMMARY + echo "- Protects branch management operations" >> $GITHUB_STEP_SUMMARY + echo "- Ensures only repository members can perform sensitive actions" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note for Contributors:** Repository maintainers will review your PR and can manually" >> $GITHUB_STEP_SUMMARY + echo "apply additional labels, assignees, or other management actions as needed." >> $GITHUB_STEP_SUMMARY + else + echo "### ๐Ÿงน Cleanup Status for Fork PR:" >> $GITHUB_STEP_SUMMARY + echo "- **Cache Cleanup** - Runner caches cleaned" >> $GITHUB_STEP_SUMMARY + echo "- **Branch Deletion** - Fork branches remain in fork repository" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Fork PR branches are managed by the contributor in their forked repository._" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + # Show results based on action type if [ "$PR_ACTION" != "closed" ]; then echo "## ๐Ÿ“‹ Actions Taken" >> $GITHUB_STEP_SUMMARY @@ -782,6 +892,12 @@ jobs: else echo "| ๐Ÿ‘ค Default Assignee | Already assigned |" >> $GITHUB_STEP_SUMMARY fi + elif [ "${{ needs.assign-assignee.result }}" = "skipped" ]; then + if [ "$IS_SAME_REPO" = "false" ]; then + echo "| ๐Ÿ‘ค Default Assignee | โ›” Skipped (Fork PR) |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ‘ค Default Assignee | Skipped |" >> $GITHUB_STEP_SUMMARY + fi fi # Welcome message @@ -798,6 +914,12 @@ jobs: if [ -n "$SIZE_LABEL" ]; then echo "| ๐Ÿ“ Size Analysis | $SIZE_LABEL ($TOTAL_CHANGES changes) |" >> $GITHUB_STEP_SUMMARY fi + elif [ "${{ needs.analyze-size.result }}" = "skipped" ]; then + if [ "$IS_SAME_REPO" = "false" ]; then + echo "| ๐Ÿ“ Size Analysis | โ›” Skipped (Fork PR) |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ“ Size Analysis | Skipped |" >> $GITHUB_STEP_SUMMARY + fi fi else @@ -813,14 +935,22 @@ jobs: fi # Branch deletion - if [ "$PR_MERGED" = "true" ] && [ "${{ needs.delete-branch.result }}" = "success" ]; then - DELETED="${{ needs.delete-branch.outputs.branch-deleted }}" - if [ "$DELETED" = "true" ]; then - echo "| ๐ŸŒฟ Branch Deletion | Deleted |" >> $GITHUB_STEP_SUMMARY - elif [ "$DELETED" = "skip" ]; then - echo "| ๐ŸŒฟ Branch Deletion | Skipped (protected) |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐ŸŒฟ Branch Deletion | Already deleted |" >> $GITHUB_STEP_SUMMARY + if [ "$PR_MERGED" = "true" ]; then + if [ "${{ needs.delete-branch.result }}" = "success" ]; then + DELETED="${{ needs.delete-branch.outputs.branch-deleted }}" + if [ "$DELETED" = "true" ]; then + echo "| ๐ŸŒฟ Branch Deletion | Deleted |" >> $GITHUB_STEP_SUMMARY + elif [ "$DELETED" = "skip" ]; then + echo "| ๐ŸŒฟ Branch Deletion | Skipped (protected) |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐ŸŒฟ Branch Deletion | Already deleted |" >> $GITHUB_STEP_SUMMARY + fi + elif [ "${{ needs.delete-branch.result }}" = "skipped" ]; then + if [ "$IS_SAME_REPO" = "false" ]; then + echo "| ๐ŸŒฟ Branch Deletion | โ›” Skipped (Fork PR - managed by contributor) |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐ŸŒฟ Branch Deletion | Skipped |" >> $GITHUB_STEP_SUMMARY + fi fi fi fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7aec89c..737f732 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -78,6 +78,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable the upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5fe9434cd24fe243e33e7f3305f8a5b519b70280 # v4.31.1 + uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: sarif_file: results.sarif diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index a487ad0..5c05c23 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -11,6 +11,14 @@ # # Maintainer: @mrz1836 # +# SECURITY MODEL: +# - Fork PRs CANNOT trigger this workflow directly (only push events trigger it) +# - Workflow only runs AFTER fork PR is merged to main by maintainer +# - Security relies on code review process: maintainer approval = trusted changes +# - All label changes are logged with commit source and author for audit trail +# - Basic validation prevents reserved label names and enforces schema compliance +# - For higher security, protect .github/labels.yml with CODEOWNERS +# # ------------------------------------------------------------------------------------ name: Sync Labels @@ -102,6 +110,9 @@ jobs: permissions: contents: read issues: write # Required for label management + outputs: + is-merge: ${{ steps.log_source.outputs.is-merge }} + pr-number: ${{ steps.log_source.outputs.pr-number }} steps: # -------------------------------------------------------------------- @@ -125,6 +136,50 @@ jobs: # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 2 # Fetch enough history to check parent commits + + # -------------------------------------------------------------------- + # Log commit source for audit trail + # -------------------------------------------------------------------- + - name: ๐Ÿ“‹ Log commit source + id: log_source + env: + COMMIT_SHA: "${{ github.sha }}" + COMMITTER_NAME: "${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.head_commit.committer.name }}" + COMMITTER_EMAIL: "${{ github.event_name == 'workflow_dispatch' && format('{0}@users.noreply.github.com', github.actor) || github.event.head_commit.committer.email }}" + AUTHOR_NAME: "${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.head_commit.author.name }}" + AUTHOR_EMAIL: "${{ github.event_name == 'workflow_dispatch' && format('{0}@users.noreply.github.com', github.actor) || github.event.head_commit.author.email }}" + COMMIT_MESSAGE: "${{ github.event_name == 'workflow_dispatch' && format('Manual label sync by {0} (dry-run: {1})', github.actor, github.event.inputs.dry_run) || github.event.head_commit.message }}" + COMMIT_TIMESTAMP: "${{ github.event_name == 'workflow_dispatch' && github.event.repository.updated_at || github.event.head_commit.timestamp }}" + run: | + echo "๐Ÿ” === Commit Source Audit ===" + echo "Commit SHA: $COMMIT_SHA" + echo "Committed by: $COMMITTER_NAME <$COMMITTER_EMAIL>" + echo "Author: $AUTHOR_NAME <$AUTHOR_EMAIL>" + echo "Message: $COMMIT_MESSAGE" + echo "Timestamp: $COMMIT_TIMESTAMP" + + # Check if this is a merge commit (has multiple parents) + PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w) + PARENT_COUNT=$((PARENT_COUNT - 1)) # Subtract 1 for the commit itself + + if [ "$PARENT_COUNT" -gt 1 ]; then + echo "Type: Merge commit (from PR or branch merge)" + echo "is-merge=true" >> $GITHUB_OUTPUT + + # Try to extract PR number from commit message + PR_NUM=$(echo "$COMMIT_MESSAGE" | grep -oP '#\K[0-9]+' | head -1) + if [ -n "$PR_NUM" ]; then + echo "PR Number: #$PR_NUM" + echo "pr-number=$PR_NUM" >> $GITHUB_OUTPUT + fi + else + echo "Type: Direct commit to main branch" + echo "is-merge=false" >> $GITHUB_OUTPUT + fi + + echo "โœ… Commit source logged for audit trail" # -------------------------------------------------------------------- # Validate and parse labels file @@ -150,6 +205,18 @@ jobs: import json import sys import os + import re + + # Security: Reserved and suspicious label names + RESERVED_NAMES = [ + 'admin', 'administrator', 'root', 'system', 'owner', + 'bypass', 'override', 'escalate', 'privilege', 'sudo', + 'critical-vulnerability', 'exploit', 'backdoor' + ] + + # Maximum lengths for GitHub labels + MAX_NAME_LENGTH = 50 + MAX_DESCRIPTION_LENGTH = 100 # GitHub allows 200, but we enforce stricter limit try: with open('${{ needs.load-env.outputs.labels-file }}', 'r') as f: @@ -163,21 +230,40 @@ jobs: # Validate all labels validation_errors = [] + validation_warnings = [] + for i, label in enumerate(labels): - if not label.get('name'): + label_name = label.get('name', '') + + if not label_name: validation_errors.append(f'Label {i + 1}: missing "name" field') + continue + # Security: Check for reserved/suspicious names + name_lower = label_name.lower() + if name_lower in RESERVED_NAMES: + validation_errors.append(f'Label "{label_name}": reserved name not allowed (security policy)') + + # Validate name length + if len(label_name) > MAX_NAME_LENGTH: + validation_errors.append(f'Label "{label_name}": name too long ({len(label_name)} > {MAX_NAME_LENGTH} chars)') + + # Validate color color = label.get('color', '') if not color: - validation_errors.append(f'Label "{label.get("name", "unknown")}": missing "color" field') + validation_errors.append(f'Label "{label_name}": missing "color" field') else: # Normalize and validate color normalized_color = color.replace('#', '').lower() if not (len(normalized_color) == 6 and all(c in '0123456789abcdef' for c in normalized_color)): - validation_errors.append(f'Label "{label.get("name", "unknown")}": invalid color "{color}" (must be 6-digit hex)') + validation_errors.append(f'Label "{label_name}": invalid color "{color}" (must be 6-digit hex)') - if not label.get('description'): - validation_errors.append(f'Label "{label.get("name", "unknown")}": missing "description" field') + # Validate description + description = label.get('description', '') + if not description: + validation_errors.append(f'Label "{label_name}": missing "description" field') + elif len(description) > MAX_DESCRIPTION_LENGTH: + validation_warnings.append(f'Label "{label_name}": description very long ({len(description)} chars, consider shortening)') if validation_errors: print('\nโŒ Validation Errors:') @@ -185,6 +271,12 @@ jobs: print(f' - {error}') sys.exit(1) + if validation_warnings: + print('\nโš ๏ธ Validation Warnings:') + for warning in validation_warnings: + print(f' - {warning}') + print('Note: Warnings do not prevent sync, but consider addressing them') + print('โœ… All labels in manifest are valid') # Convert to JSON and output for github-script @@ -494,6 +586,15 @@ jobs: # Generate a workflow summary report # -------------------------------------------------------------------- - name: ๐Ÿ“Š Generate workflow summary + env: + LABELS_FILE: ${{ needs.load-env.outputs.labels-file }} + DRY_RUN_MODE: ${{ github.event.inputs.dry_run == 'true' && '๐Ÿ” DRY RUN' || '๐Ÿš€ LIVE' }} + TRIGGER_TYPE: ${{ github.event_name == 'workflow_dispatch' && '๐Ÿ”ง Manual' || '๐Ÿ“ File Change' }} + COMMIT_SHA: ${{ github.sha }} + COMMITTER_NAME: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.head_commit.committer.name }} + AUTHOR_NAME: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.head_commit.author.name }} + IS_MERGE: ${{ steps.log_source.outputs.is-merge }} + PR_NUMBER: ${{ steps.log_source.outputs.pr-number }} run: | echo "๐Ÿš€ Generating workflow summary..." @@ -505,9 +606,25 @@ jobs: echo "## โš™๏ธ Configuration" >> $GITHUB_STEP_SUMMARY echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Labels file | \`${{ needs.load-env.outputs.labels-file }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Mode | ${{ github.event.inputs.dry_run == 'true' && '๐Ÿ” DRY RUN' || '๐Ÿš€ LIVE' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Trigger | ${{ github.event_name == 'workflow_dispatch' && '๐Ÿ”ง Manual' || '๐Ÿ“ File Change' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Labels file | \`$LABELS_FILE\` |" >> $GITHUB_STEP_SUMMARY + echo "| Mode | $DRY_RUN_MODE |" >> $GITHUB_STEP_SUMMARY + echo "| Trigger | $TRIGGER_TYPE |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "## ๐Ÿ“‹ Commit Source (Audit Trail)" >> $GITHUB_STEP_SUMMARY + echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Commit SHA | \`$COMMIT_SHA\` |" >> $GITHUB_STEP_SUMMARY + echo "| Committer | $COMMITTER_NAME |" >> $GITHUB_STEP_SUMMARY + echo "| Author | $AUTHOR_NAME |" >> $GITHUB_STEP_SUMMARY + if [ "$IS_MERGE" = "true" ]; then + echo "| Type | ๐Ÿ”€ Merge commit (from PR) |" >> $GITHUB_STEP_SUMMARY + if [ -n "$PR_NUMBER" ]; then + echo "| PR Number | #$PR_NUMBER |" >> $GITHUB_STEP_SUMMARY + fi + else + echo "| Type | ๐Ÿ“ Direct commit to main |" >> $GITHUB_STEP_SUMMARY + fi echo "" >> $GITHUB_STEP_SUMMARY echo "## ๐Ÿ“Š Results" >> $GITHUB_STEP_SUMMARY