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