From c35420f8926a70c01b6b7b6c0267c42abf8dc5de Mon Sep 17 00:00:00 2001 From: mrz1836 Date: Mon, 1 Jun 2026 08:20:18 -0400 Subject: [PATCH] sync(ci): update GitHub workflows and actions config --- .dockerignore | 2 + .github/.yamlfmt | 5 +- .github/CODEOWNERS | 4 +- .github/actions/load-env/action.yml | 1 + .github/actions/setup-goreleaser/action.yml | 2 +- .github/actions/setup-magex/action.yml | 39 +- .../actions/validate-test-results/action.yml | 359 +++++ .github/env/10-coverage.env | 2 +- .github/env/10-mage-x.env | 7 +- .github/env/10-pre-commit.env | 2 +- .github/workflows/auto-merge-on-approval.yml | 317 ++--- .github/workflows/codeql-analysis.yml | 3 + .github/workflows/dependabot-auto-merge.yml | 322 ++--- .github/workflows/fortress-benchmarks.yml | 2 + .github/workflows/fortress-code-quality.yml | 555 ++++---- .../fortress-completion-finalize.yml | 381 ----- .../workflows/fortress-completion-report.yml | 1251 ++++++++++++++++- .../fortress-completion-statistics.yml | 722 ---------- .../workflows/fortress-completion-tests.yml | 476 ------- .github/workflows/fortress-coverage.yml | 78 +- .github/workflows/fortress-pre-commit.yml | 2 + .github/workflows/fortress-release.yml | 1 + .github/workflows/fortress-security-scans.yml | 741 +++++----- .github/workflows/fortress-setup-config.yml | 322 ++++- .github/workflows/fortress-test-fuzz.yml | 2 + .github/workflows/fortress-test-magex.yml | 278 ---- .github/workflows/fortress-test-matrix.yml | 2 + .github/workflows/fortress-test-suite.yml | 50 +- .../workflows/fortress-test-validation.yml | 420 ------ .github/workflows/fortress-warm-cache.yml | 1 + .github/workflows/fortress.yml | 175 +-- .../pull-request-management-fork.yml | 536 ------- .github/workflows/pull-request-management.yml | 1111 ++++++++------- .github/workflows/scorecard.yml | 3 +- .github/workflows/stale-check.yml | 113 +- .github/workflows/sync-labels.yml | 8 +- 36 files changed, 3551 insertions(+), 4744 deletions(-) create mode 100644 .github/actions/validate-test-results/action.yml delete mode 100644 .github/workflows/fortress-completion-finalize.yml delete mode 100644 .github/workflows/fortress-completion-statistics.yml delete mode 100644 .github/workflows/fortress-completion-tests.yml delete mode 100644 .github/workflows/fortress-test-magex.yml delete mode 100644 .github/workflows/fortress-test-validation.yml delete mode 100644 .github/workflows/pull-request-management-fork.yml diff --git a/.dockerignore b/.dockerignore index 3d6870a..56b8674 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,6 +22,8 @@ vendor/ # Binaries for programs and plugins dist/ !dist/linux/ +!dist/linux-amd64/ +!dist/linux-arm64/ gin-bin *.exe *.exe~ diff --git a/.github/.yamlfmt b/.github/.yamlfmt index 559f8bb..0b5a7e2 100644 --- a/.github/.yamlfmt +++ b/.github/.yamlfmt @@ -3,8 +3,6 @@ # # Purpose: YAML formatting configuration for the mage-x (yamlfmt) tool # -# Maintainer: @mrz1836 -# # ------------------------------------------------------------------------------------ formatter: @@ -74,6 +72,9 @@ exclude: - "**/*.swo" - "**/*~" + # Test fixtures (intentionally malformed YAML used by ci-tester). + - ".github/ci-tester/fixtures/workflow-invalid/.github/workflows/invalid.yml" + # Environment files - "**/env/**" - "**/.env.base" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e305095..02b5be5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,8 +10,6 @@ .github/scripts/* @mrz1836 .github/workflows/* @mrz1836 .github/env/* @mrz1836 -.github/.env.base @mrz1836 -.github/.env.custom @mrz1836 # MAGE-X .mage.yaml @mrz1836 @@ -43,7 +41,7 @@ codecov.yml @mrz1836 # Security and configuration files .github/SECURITY.md @mrz1836 -.github/.gitleaks.toml @mrz1836 +.gitleaksignore @mrz1836 # Repository configuration .github/labels.yml @mrz1836 diff --git a/.github/actions/load-env/action.yml b/.github/actions/load-env/action.yml index ec348ad..dc40121 100644 --- a/.github/actions/load-env/action.yml +++ b/.github/actions/load-env/action.yml @@ -46,6 +46,7 @@ runs: id: load-env shell: bash run: | + set -euo pipefail echo "๐Ÿ“‹ Loading environment configuration..." LOADER_SCRIPT=".github/env/load-env.sh" diff --git a/.github/actions/setup-goreleaser/action.yml b/.github/actions/setup-goreleaser/action.yml index 4c6ea9d..7ef079c 100644 --- a/.github/actions/setup-goreleaser/action.yml +++ b/.github/actions/setup-goreleaser/action.yml @@ -105,7 +105,7 @@ runs: # -------------------------------------------------------------------- - name: โœ… Install GoReleaser (cache miss) if: steps.check-existing.outputs.exists != 'true' && steps.goreleaser-cache.outputs.cache-hit != 'true' - uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1 + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: distribution: goreleaser version: ${{ inputs.goreleaser-version }} diff --git a/.github/actions/setup-magex/action.yml b/.github/actions/setup-magex/action.yml index cd116c2..6693bbd 100644 --- a/.github/actions/setup-magex/action.yml +++ b/.github/actions/setup-magex/action.yml @@ -156,12 +156,49 @@ runs: --pattern "$ASSET_NAME" \ --dir .; then echo "โœ… Download successful" - mv "$ASSET_NAME" mage-x.tar.gz else echo "โŒ Download failed for $ASSET_NAME from mrz1836/mage-x@$VERSION" exit 1 fi + # Verify SHA256 integrity against the release's checksums file before extraction. + # Without this, a compromised release asset would silently be executed. + # GoReleaser names the file mage-x_${VERSION}_checksums.txt by default. + CHECKSUMS_FILE="mage-x_${CLEAN_VERSION}_checksums.txt" + echo "๐Ÿ” Verifying SHA256 checksum against $CHECKSUMS_FILE..." + if ! gh release download "$VERSION" \ + --repo mrz1836/mage-x \ + --pattern "$CHECKSUMS_FILE" \ + --dir .; then + echo "โŒ Failed to download $CHECKSUMS_FILE from mrz1836/mage-x@$VERSION" + echo "โŒ Cannot verify binary integrity โ€” refusing to proceed" + exit 1 + fi + + EXPECTED_HASH=$(grep " ${ASSET_NAME}\$" "$CHECKSUMS_FILE" | awk '{print $1}') + if [[ -z "$EXPECTED_HASH" ]]; then + echo "โŒ No checksum entry found for $ASSET_NAME in $CHECKSUMS_FILE" + echo "๐Ÿ“‹ $CHECKSUMS_FILE contents:" + cat "$CHECKSUMS_FILE" + exit 1 + fi + + if command -v sha256sum >/dev/null 2>&1; then + ACTUAL_HASH=$(sha256sum "$ASSET_NAME" | awk '{print $1}') + else + ACTUAL_HASH=$(shasum -a 256 "$ASSET_NAME" | awk '{print $1}') + fi + + if [[ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]]; then + echo "โŒ Checksum mismatch for $ASSET_NAME" + echo " Expected: $EXPECTED_HASH" + echo " Actual: $ACTUAL_HASH" + exit 1 + fi + echo "โœ… SHA256 checksum verified: $ACTUAL_HASH" + + mv "$ASSET_NAME" mage-x.tar.gz + # Extract the tarball if tar -xzf mage-x.tar.gz; then echo "โœ… Extraction successful" diff --git a/.github/actions/validate-test-results/action.yml b/.github/actions/validate-test-results/action.yml new file mode 100644 index 0000000..9e1b2b3 --- /dev/null +++ b/.github/actions/validate-test-results/action.yml @@ -0,0 +1,359 @@ +name: "Validate Test Results" +description: "Download test result artifacts and validate them, producing a step summary and failing the workflow if any test failed." + +# Inputs mirror what fortress-test-validation.yml's reusable workflow took. +# This composite action is invoked from inside fortress-coverage.yml jobs +# (process-coverage and codecov-upload) since the standalone validation +# workflow was retired to save billing minutes. +inputs: + fuzz-testing-enabled: + description: "Whether fuzz testing is enabled" + required: true + +runs: + using: "composite" + steps: + # -------------------------------------------------------------------- + # Download CI results from test matrix + # -------------------------------------------------------------------- + - name: ๐Ÿ“ฅ Download CI results + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "ci-results-*" + path: ci-results/ + merge-multiple: true + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} + + # -------------------------------------------------------------------- + # Download fuzz test results if enabled + # -------------------------------------------------------------------- + - name: ๐Ÿ“ฅ Download fuzz test results + if: inputs.fuzz-testing-enabled == 'true' + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "test-results-fuzz-*" + path: ci-results/ + merge-multiple: true + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} + + # -------------------------------------------------------------------- + # Validate test results from CI mode JSONL output + # -------------------------------------------------------------------- + - name: ๐Ÿ” Validate test results + shell: bash + run: | + set -uo pipefail + echo "๐Ÿ” Validating test results from CI mode output..." + source .github/scripts/parse-test-label.sh + + VALIDATION_FAILED=false + TOTAL_FAILURES=0 + TOTAL_UNIQUE=0 + TOTAL_TESTS=0 + TOTAL_SKIPPED=0 + + # Find all CI results files + echo "๐Ÿ“‹ Looking for CI results files..." + find ci-results/ -name "ci-results.jsonl" -o -name "*.jsonl" 2>/dev/null | head -20 + + # Process each CI results file + if find ci-results/ -name "*.jsonl" 2>/dev/null | grep -q .; then + echo "โœ… Found CI results JSONL files" + + while IFS= read -r -d '' jsonl_file; do + # Extract artifact directory name from JSONL file path + # Supported directory structures: + # 1. Expected: ci-results/ARTIFACT_NAME/.mage-x/ci-results.jsonl + # โ†’ Use grandparent (skip .mage-x) to get ARTIFACT_NAME + # 2. Fallback: ci-results/ARTIFACT_NAME/ci-results.jsonl + # โ†’ Use parent directory as ARTIFACT_NAME + ARTIFACT_DIR=$(dirname "$(dirname "$jsonl_file")" | xargs basename) + JSONL_NAME=$(basename "$jsonl_file") + + # Detect which structure we have by checking parent directory + PARENT_DIR=$(basename "$(dirname "$jsonl_file")") + if [[ "$PARENT_DIR" != ".mage-x" ]]; then + echo " Warning: Unexpected artifact structure for: $jsonl_file" + echo " Expected: ci-results/ARTIFACT_NAME/.mage-x/ci-results.jsonl" + # Fallback: parent is the artifact dir (not grandparent) + ARTIFACT_DIR=$(basename "$(dirname "$jsonl_file")") + fi + + TEST_LABEL=$(parse_test_label "$ARTIFACT_DIR" "$JSONL_NAME") + + echo "" + echo "๐Ÿ“„ Processing: $TEST_LABEL" + + # Extract summary line (type: summary) + SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") + + if [[ -n "$SUMMARY" ]]; then + STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') + PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') + FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') + SKIPPED=$(echo "$SUMMARY" | jq -r '.summary.skipped // 0') + UNIQUE=$(echo "$SUMMARY" | jq -r '.summary.unique_total // 0') + TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') + DURATION=$(echo "$SUMMARY" | jq -r '.summary.duration // "unknown"') + + echo " โ€ข Status: $STATUS" + echo " โ€ข Passed: $PASSED" + echo " โ€ข Failed: $FAILED" + echo " โ€ข Skipped: $SKIPPED" + echo " โ€ข Unique Tests: $UNIQUE" + echo " โ€ข Test Runs: $TOTAL" + echo " โ€ข Duration: $DURATION" + + TOTAL_UNIQUE=$((TOTAL_UNIQUE + UNIQUE)) + TOTAL_TESTS=$((TOTAL_TESTS + TOTAL)) + TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED)) + + if [[ "$STATUS" == "failed" ]] || [[ "$FAILED" -gt 0 ]]; then + VALIDATION_FAILED=true + TOTAL_FAILURES=$((TOTAL_FAILURES + FAILED)) + + # Extract failure details + echo "" + echo " ๐Ÿšจ Failures in this file:" + grep '"type":"failure"' "$jsonl_file" 2>/dev/null | while read -r line; do + TEST=$(echo "$line" | jq -r '.failure.test // "unknown"') + PKG=$(echo "$line" | jq -r '.failure.package // "unknown"' | sed 's|.*/||') + FILE=$(echo "$line" | jq -r '.failure.file // ""') + LINE_NUM=$(echo "$line" | jq -r '.failure.line // ""') + FAIL_TYPE=$(echo "$line" | jq -r '.failure.type // "test"') + ERROR_MSG=$(echo "$line" | jq -r '.failure.error // ""') + + if [[ -n "$FILE" && -n "$LINE_NUM" && "$LINE_NUM" != "0" ]]; then + echo " โŒ [$FAIL_TYPE] $TEST ($PKG) at $FILE:$LINE_NUM" + else + echo " โŒ [$FAIL_TYPE] $TEST ($PKG)" + fi + + if [[ -n "$ERROR_MSG" && "$ERROR_MSG" != "null" ]]; then + echo " โ†’ ${ERROR_MSG:0:200}" + fi + done | head -30 + fi + else + echo " โš ๏ธ No summary found in JSONL file" + + FAILURE_COUNT=$(grep -c '"type":"failure"' "$jsonl_file" 2>/dev/null || echo "0") + if [[ "$FAILURE_COUNT" -gt 0 ]]; then + echo " โ€ข Found $FAILURE_COUNT failure entries" + VALIDATION_FAILED=true + TOTAL_FAILURES=$((TOTAL_FAILURES + FAILURE_COUNT)) + fi + fi + done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) + else + echo "โš ๏ธ No JSONL files found - checking for test-output.log files..." + + # Fallback: check test-output.log files for exit codes + while IFS= read -r -d '' log_file; do + echo "๐Ÿ“„ Checking: $log_file" + + if grep -q "^FAIL" "$log_file" 2>/dev/null || grep -q "--- FAIL:" "$log_file" 2>/dev/null; then + echo " โŒ Found test failures in log file" + VALIDATION_FAILED=true + FAIL_COUNT=$(grep -c "^--- FAIL:" "$log_file" 2>/dev/null || echo "1") + TOTAL_FAILURES=$((TOTAL_FAILURES + FAIL_COUNT)) + fi + done < <(find ci-results/ -name "test-output.log" -print0 2>/dev/null) + fi + + # Final validation result + echo "" + echo "๐Ÿ Validation Summary:" + echo " โ€ข Unique Tests: $TOTAL_UNIQUE" + echo " โ€ข Test Runs: $TOTAL_TESTS" + echo " โ€ข Total Failures: $TOTAL_FAILURES" + echo " โ€ข Total Skipped: $TOTAL_SKIPPED" + echo " โ€ข Validation Status: $(if [[ "$VALIDATION_FAILED" == "true" ]]; then echo "FAILED"; else echo "PASSED"; fi)" + + if [[ "$VALIDATION_FAILED" == "true" ]]; then + echo "" + echo "โŒ Test validation failed - $TOTAL_FAILURES failure(s) detected" + echo "::error title=Test Validation Failed::$TOTAL_FAILURES test failure(s) detected. Check the CI results above for details." + exit 1 + else + echo "" + echo "โœ… All tests passed validation" + fi + + # -------------------------------------------------------------------- + # Create validation summary for GitHub UI + # -------------------------------------------------------------------- + - name: ๐Ÿ“Š Create validation summary + if: always() + shell: bash + env: + FUZZ_TESTING_ENABLED: ${{ inputs.fuzz-testing-enabled }} + JOB_STATUS: ${{ job.status }} + run: | + set -uo pipefail + source .github/scripts/parse-test-label.sh + + echo "## ๐Ÿ” Test Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count artifacts + MATRIX_JOBS=$(find ci-results/ -name "*.jsonl" 2>/dev/null | wc -l || echo "0") + echo "- **Matrix Jobs Validated**: $MATRIX_JOBS" >> $GITHUB_STEP_SUMMARY + + # Aggregate results from JSONL files + TOTAL_PASSED=0 + TOTAL_FAILED=0 + TOTAL_SKIPPED=0 + TOTAL_UNIQUE=0 + TOTAL_TESTS=0 + + while IFS= read -r -d '' jsonl_file; do + SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") + if [[ -n "$SUMMARY" ]]; then + PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') + FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') + SKIPPED=$(echo "$SUMMARY" | jq -r '.summary.skipped // 0') + UNIQUE=$(echo "$SUMMARY" | jq -r '.summary.unique_total // 0') + TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') + TOTAL_PASSED=$((TOTAL_PASSED + PASSED)) + TOTAL_FAILED=$((TOTAL_FAILED + FAILED)) + TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED)) + TOTAL_UNIQUE=$((TOTAL_UNIQUE + UNIQUE)) + TOTAL_TESTS=$((TOTAL_TESTS + TOTAL)) + fi + done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) + + echo "- **Unique Tests**: $TOTAL_UNIQUE" >> $GITHUB_STEP_SUMMARY + echo "- **Test Runs**: $TOTAL_TESTS" >> $GITHUB_STEP_SUMMARY + echo "- **Passed**: $TOTAL_PASSED" >> $GITHUB_STEP_SUMMARY + echo "- **Failed**: $TOTAL_FAILED" >> $GITHUB_STEP_SUMMARY + echo "- **Skipped**: $TOTAL_SKIPPED" >> $GITHUB_STEP_SUMMARY + echo "- **Validation Status**: $JOB_STATUS" >> $GITHUB_STEP_SUMMARY + + if [[ "$FUZZ_TESTING_ENABLED" == "true" ]]; then + echo "- **Fuzz Testing**: Enabled" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Show per-job breakdown + if [[ $MATRIX_JOBS -gt 0 ]]; then + echo "### Test Matrix Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + while IFS= read -r -d '' jsonl_file; do + ARTIFACT_DIR=$(dirname "$(dirname "$jsonl_file")" | xargs basename) + JSONL_NAME=$(basename "$jsonl_file") + + PARENT_DIR=$(basename "$(dirname "$jsonl_file")") + if [[ "$PARENT_DIR" != ".mage-x" ]]; then + ARTIFACT_DIR=$(basename "$(dirname "$jsonl_file")") + fi + + TEST_LABEL=$(parse_test_label "$ARTIFACT_DIR" "$JSONL_NAME") + + SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") + + if [[ -n "$SUMMARY" ]]; then + STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') + PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') + FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') + + if [[ "$STATUS" == "passed" ]]; then + echo "- โœ… **$TEST_LABEL**: $PASSED tests passed" >> $GITHUB_STEP_SUMMARY + else + echo "- โŒ **$TEST_LABEL**: $FAILED failures" >> $GITHUB_STEP_SUMMARY + fi + fi + done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) + fi + + # Add detailed failure section if there are failures + if [[ $TOTAL_FAILED -gt 0 ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿšจ Failure Details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Expand each failure to see full output and stack traces_" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + FAILURE_COUNT=0 + while IFS= read -r -d '' jsonl_file; do + while read -r line; do + FAILURE_COUNT=$((FAILURE_COUNT + 1)) + if [[ $FAILURE_COUNT -gt 20 ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "_... additional failures truncated_" >> $GITHUB_STEP_SUMMARY + break 2 + fi + + TEST=$(echo "$line" | jq -r '.failure.test // "unknown"') + PKG=$(echo "$line" | jq -r '.failure.package // "unknown"' | sed 's|.*/||') + FAIL_TYPE=$(echo "$line" | jq -r '.failure.type // "test"') + ERROR_MSG=$(echo "$line" | jq -r '.failure.error // ""') + OUTPUT=$(echo "$line" | jq -r '.failure.output // ""') + STACK=$(echo "$line" | jq -r '.failure.stack // ""') + + echo "
" >> $GITHUB_STEP_SUMMARY + echo "โŒ $TEST ($PKG) - $FAIL_TYPE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ -n "$ERROR_MSG" && "$ERROR_MSG" != "null" ]]; then + echo "**Error:** \`$ERROR_MSG\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [[ -n "$OUTPUT" && "$OUTPUT" != "null" && "$OUTPUT" != "" ]]; then + echo "**Output:**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${OUTPUT:0:2000}" >> $GITHUB_STEP_SUMMARY + if [[ ${#OUTPUT} -gt 2000 ]]; then + echo "... (truncated)" >> $GITHUB_STEP_SUMMARY + fi + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + if [[ -n "$STACK" && "$STACK" != "null" && "$STACK" != "" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Stack Trace:**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${STACK:0:1500}" >> $GITHUB_STEP_SUMMARY + if [[ ${#STACK} -gt 1500 ]]; then + echo "... (truncated)" >> $GITHUB_STEP_SUMMARY + fi + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + # For fuzz tests, show fuzz-specific info + FUZZ_INFO=$(echo "$line" | jq -r '.failure.fuzz_info // null') + if [[ "$FUZZ_INFO" != "null" && -n "$FUZZ_INFO" ]]; then + CORPUS=$(echo "$FUZZ_INFO" | jq -r '.corpus_path // ""') + if [[ -n "$CORPUS" && "$CORPUS" != "null" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Fuzz Corpus:** \`$CORPUS\`" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + done < <(grep '"type":"failure"' "$jsonl_file" 2>/dev/null) + done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) + fi + + # -------------------------------------------------------------------- + # Upload validation artifacts + # -------------------------------------------------------------------- + - name: ๐Ÿ“ค Upload validation summary + if: always() + uses: ./.github/actions/upload-artifact-resilient + with: + artifact-name: validation-summary + artifact-path: ci-results/ + retention-days: "7" + if-no-files-found: ignore diff --git a/.github/env/10-coverage.env b/.github/env/10-coverage.env index 852823a..7fa5f12 100644 --- a/.github/env/10-coverage.env +++ b/.github/env/10-coverage.env @@ -32,7 +32,7 @@ GO_COVERAGE_PROVIDER=internal CODECOV_TOKEN_REQUIRED=false # Go Coverage Tool Version -GO_COVERAGE_VERSION=v1.4.0 +GO_COVERAGE_VERSION=v1.5.0 GO_COVERAGE_USE_LOCAL=false # ================================================================================================ diff --git a/.github/env/10-mage-x.env b/.github/env/10-mage-x.env index aff6aa9..0661584 100644 --- a/.github/env/10-mage-x.env +++ b/.github/env/10-mage-x.env @@ -36,7 +36,7 @@ # ================================================================================================ # MAGE-X version -MAGE_X_VERSION=v1.21.0 +MAGE_X_VERSION=v1.24.0 # For mage-x development, set to 'true' to use local version instead of downloading from releases MAGE_X_USE_LOCAL=false @@ -50,6 +50,9 @@ MAGE_X_CI_SKIP_STEP_SUMMARY=false MAGE_X_AUTO_DISCOVER_BUILD_TAGS=true MAGE_X_AUTO_DISCOVER_BUILD_TAGS_EXCLUDE=race,custom +# Run all discovered tags in a single test pass (fast). Set to false in a +# project env file to fall back to one separate pass per tag. +MAGE_X_AUTO_DISCOVER_BUILD_TAGS_COMBINE=true MAGE_X_FORMAT_EXCLUDE_PATHS=vendor,node_modules,.git,.idea # Exclude magefiles from prebuild - they require 'mage' build tag and fail without it @@ -62,7 +65,7 @@ MAGE_X_FORMAT_EXCLUDE_PATHS=vendor,node_modules,.git,.idea MAGE_X_GITLEAKS_VERSION=8.30.1 MAGE_X_GOFUMPT_VERSION=v0.10.0 MAGE_X_GOLANGCI_LINT_VERSION=v2.12.2 -MAGE_X_GORELEASER_VERSION=v2.15.4 +MAGE_X_GORELEASER_VERSION=v2.16.0 MAGE_X_GOVULNCHECK_VERSION=v1.1.4 MAGE_X_GO_SECONDARY_VERSION=1.24.x MAGE_X_GO_VERSION=1.24.x diff --git a/.github/env/10-pre-commit.env b/.github/env/10-pre-commit.env index bde02ef..39896d3 100644 --- a/.github/env/10-pre-commit.env +++ b/.github/env/10-pre-commit.env @@ -26,7 +26,7 @@ # ๐Ÿช PRE-COMMIT TOOL VERSION # ================================================================================================ -GO_PRE_COMMIT_VERSION=v1.8.3 +GO_PRE_COMMIT_VERSION=v1.9.0 GO_PRE_COMMIT_USE_LOCAL=false # ================================================================================================ diff --git a/.github/workflows/auto-merge-on-approval.yml b/.github/workflows/auto-merge-on-approval.yml index db4521e..3ce3329 100644 --- a/.github/workflows/auto-merge-on-approval.yml +++ b/.github/workflows/auto-merge-on-approval.yml @@ -51,22 +51,57 @@ concurrency: jobs: # ---------------------------------------------------------------------------------- - # Load Environment Variables + # Auto-merge processing (single consolidated job) + # + # Workflow phases (each preserved as a step): + # ๐ŸŒ Load environment โ†’ reads .github/env/* + # ๐Ÿ”ง Extract configuration โ†’ exports AUTO_MERGE_* vars to env + # ๐Ÿค– Process auto-merge โ†’ applies conditions and enables/disables auto-merge + # ๐Ÿ“Š Generate summary โ†’ always # ---------------------------------------------------------------------------------- - load-env: - name: ๐ŸŒ Load Environment Variables - runs-on: ubuntu-latest + auto-merge: + name: ๐Ÿค– Auto-merge on Approval + # ---------------------------------------------------------------- + # Event gate: skip review events that can't meaningfully + # change the auto-merge decision. + # + # `pull_request_review:submitted` fires for ALL review states โ€” + # "approved", "changes_requested", "commented", and "dismissed". + # Only the first two affect auto-merge state. A pure "commented" + # review (very common โ€” "lgtm but one question") would otherwise + # trigger a full workflow run that just exits at conditions-not-met. + # Same for "dismissed" reviews. + # + # The first clause keeps the non-review triggers + # (ready_for_review, review_request_removed) running normally โ€” + # both are real state changes that must re-evaluate the PR. + # ---------------------------------------------------------------- + if: | + github.event_name != 'pull_request_review' || + contains(fromJSON('["approved", "changes_requested"]'), github.event.review.state) + runs-on: ubuntu-24.04 permissions: - contents: read - outputs: - env-json: ${{ steps.load-env.outputs.env-json }} + contents: read # Required: Sparse checkout for env files + pull-requests: write # Required: Update PR status and enable auto-merge + issues: write # Required: Add labels and create comments + steps: # -------------------------------------------------------------------- # Check out code to access env file # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code (sparse) + # SECURITY: pin the checkout to the trusted base ref so the local + # ./.github/actions/load-env action (run below with pull-requests/issues write) + # can never be a PR-controlled version. Default checkout on pull_request / + # pull_request_review events resolves to the PR merge ref, which is attacker-influenced. + # `github.base_ref` is ONLY populated for pull_request / pull_request_target โ€” it is + # empty on pull_request_review, where `github.ref` is refs/pull//merge (PR-controlled). + # Read the base branch directly from the event payload so it is the trusted base ref for + # BOTH triggers. Env files and the action are not modified there, so base-ref loading is safe. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: ${{ github.event.pull_request.base.ref }} + persist-credentials: false sparse-checkout: | .github/env .github/actions/load-env @@ -75,98 +110,55 @@ jobs: # Load and parse environment file # -------------------------------------------------------------------- - name: ๐ŸŒ Load environment variables - uses: ./.github/actions/load-env id: load-env + uses: ./.github/actions/load-env - # ---------------------------------------------------------------------------------- - # Process Auto-merge - # ---------------------------------------------------------------------------------- - process-auto-merge: - name: ๐Ÿค– Process Auto-merge - needs: [load-env] - runs-on: ubuntu-latest - permissions: - pull-requests: write # Required: Update PR status and enable auto-merge - issues: write # Required: Add labels and create comments - outputs: - action-taken: ${{ steps.process.outputs.action }} - pr-number: ${{ github.event.pull_request.number }} - - steps: # -------------------------------------------------------------------- - # Extract configuration from env-json + # Extract auto-merge configuration up-front # -------------------------------------------------------------------- - name: ๐Ÿ”ง Extract configuration - id: config env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | echo "๐Ÿ“‹ Extracting auto-merge configuration from environment..." - # Extract all needed variables + # Single jq pass for all config we need across steps MIN_APPROVALS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_MIN_APPROVALS') - REQUIRE_ALL_REVIEWS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_REQUIRE_ALL_REQUESTED_REVIEWS') - MERGE_TYPES=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_ALLOWED_MERGE_TYPES') - DELETE_BRANCH=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_DELETE_BRANCH') - SKIP_DRAFT=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_DRAFT') - SKIP_WIP=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_WIP') - WIP_LABELS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_WIP_LABELS') - COMMENT_ON_ENABLE=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_COMMENT_ON_ENABLE') - 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') - AUTO_MERGE_REQUIRE_LABEL=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_REQUIRE_LABEL') - AUTO_MERGE_LABEL=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_LABEL') - PREFERRED_TOKEN=$(echo "$ENV_JSON" | jq -r '.PREFERRED_GITHUB_TOKEN') - - # Validate required configuration if [[ -z "$MIN_APPROVALS" ]] || [[ "$MIN_APPROVALS" == "null" ]]; then MIN_APPROVALS="1" # Default to 1 approval fi - # Set as environment variables for all subsequent steps - echo "MIN_APPROVALS=$MIN_APPROVALS" >> $GITHUB_ENV - echo "REQUIRE_ALL_REVIEWS=$REQUIRE_ALL_REVIEWS" >> $GITHUB_ENV - echo "MERGE_TYPES=$MERGE_TYPES" >> $GITHUB_ENV - echo "DELETE_BRANCH=$DELETE_BRANCH" >> $GITHUB_ENV - echo "SKIP_DRAFT=$SKIP_DRAFT" >> $GITHUB_ENV - echo "SKIP_WIP=$SKIP_WIP" >> $GITHUB_ENV - echo "WIP_LABELS=$WIP_LABELS" >> $GITHUB_ENV - echo "COMMENT_ON_ENABLE=$COMMENT_ON_ENABLE" >> $GITHUB_ENV - 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 - echo "AUTO_MERGE_REQUIRE_LABEL=$AUTO_MERGE_REQUIRE_LABEL" >> $GITHUB_ENV - echo "AUTO_MERGE_LABEL=$AUTO_MERGE_LABEL" >> $GITHUB_ENV - - # Determine default merge type + MERGE_TYPES=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_ALLOWED_MERGE_TYPES') DEFAULT_MERGE_TYPE=$(echo "$MERGE_TYPES" | cut -d',' -f1) if [[ -z "$DEFAULT_MERGE_TYPE" ]]; then DEFAULT_MERGE_TYPE="squash" fi - echo "DEFAULT_MERGE_TYPE=$DEFAULT_MERGE_TYPE" >> $GITHUB_ENV - # Log configuration + { + echo "MIN_APPROVALS=$MIN_APPROVALS" + echo "REQUIRE_ALL_REVIEWS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_REQUIRE_ALL_REQUESTED_REVIEWS')" + echo "MERGE_TYPES=$MERGE_TYPES" + echo "DEFAULT_MERGE_TYPE=$DEFAULT_MERGE_TYPE" + echo "DELETE_BRANCH=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_DELETE_BRANCH')" + echo "SKIP_DRAFT=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_DRAFT')" + echo "SKIP_WIP=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_WIP')" + echo "WIP_LABELS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_WIP_LABELS')" + echo "COMMENT_ON_ENABLE=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_COMMENT_ON_ENABLE')" + echo "COMMENT_ON_DISABLE=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_COMMENT_ON_DISABLE')" + echo "LABELS_TO_ADD=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_LABELS_TO_ADD')" + echo "SKIP_BOT_PRS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_BOT_PRS')" + echo "SKIP_FORK_PRS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_FORK_PRS')" + echo "COMMENT_ON_FORK_SKIP=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_COMMENT_ON_FORK_SKIP')" + echo "AUTO_MERGE_REQUIRE_LABEL=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_REQUIRE_LABEL')" + echo "AUTO_MERGE_LABEL=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_LABEL')" + } >> "$GITHUB_ENV" + echo "๐Ÿ” Configuration loaded:" echo " โœ… Min approvals: $MIN_APPROVALS" - echo " ๐Ÿ‘ฅ Require all reviews: $REQUIRE_ALL_REVIEWS" echo " ๐Ÿ”€ Merge types: $MERGE_TYPES (default: $DEFAULT_MERGE_TYPE)" - echo " ๐Ÿ—‘๏ธ Delete branch: $DELETE_BRANCH" - echo " ๐Ÿ“ Skip draft: $SKIP_DRAFT" - echo " ๐Ÿšง Skip WIP: $SKIP_WIP" - echo " ๐Ÿท๏ธ WIP labels: $WIP_LABELS" - echo " ๐Ÿ’ฌ Comment on enable: $COMMENT_ON_ENABLE" - echo " ๐Ÿ’ฌ Comment on disable: $COMMENT_ON_DISABLE" - echo " ๐Ÿท๏ธ Labels to add: $LABELS_TO_ADD" - echo " ๐Ÿค– Skip bot PRs: $SKIP_BOT_PRS" - echo " ๐Ÿด Skip fork PRs: $SKIP_FORK_PRS" - echo " ๐Ÿ’ฌ Comment on fork skip: $COMMENT_ON_FORK_SKIP" - echo " ๐Ÿท๏ธ Require automerge label: $AUTO_MERGE_REQUIRE_LABEL" - echo " ๐Ÿท๏ธ Automerge label name: $AUTO_MERGE_LABEL" + echo " ๐Ÿค– Skip bot PRs: $(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_BOT_PRS')" + echo " ๐Ÿด Skip fork PRs: $(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_SKIP_FORK_PRS')" + echo " ๐Ÿท๏ธ Require automerge label: $(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_REQUIRE_LABEL')" echo " ๐Ÿ”‘ Token: Selected via github-script action" # -------------------------------------------------------------------- @@ -231,9 +223,9 @@ jobs: 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 + // Fork PR welcome/labeling is handled by pull-request-management.yml's fork job if (process.env.COMMENT_ON_FORK_SKIP === 'true') { - console.log(' โ„น๏ธ Comment posting skipped for fork PR (handled by fork PR workflow)'); + console.log(' โ„น๏ธ Comment posting skipped for fork PR (handled by PR mgmt fork job)'); } core.setOutput('action', 'skip-fork'); @@ -487,138 +479,81 @@ jobs: throw error; } - # ---------------------------------------------------------------------------------- - # Generate Workflow Summary Report - # ---------------------------------------------------------------------------------- - summary: - name: ๐Ÿ“Š Generate Summary - if: always() - needs: [load-env, process-auto-merge] - runs-on: ubuntu-latest - steps: # -------------------------------------------------------------------- # Generate a workflow summary report # -------------------------------------------------------------------- - name: ๐Ÿ“Š Generate workflow summary + if: always() env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + PR_NUMBER: ${{ github.event.pull_request.number }} + ACTION: ${{ steps.process.outputs.action }} run: | echo "๐Ÿ“Š Generating workflow summary..." - echo "# ๐Ÿค– Auto-merge on Approval Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**โฐ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY - echo "**๐Ÿ“‹ PR:** #${{ needs.process-auto-merge.outputs.pr-number }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - # Determine action taken - ACTION="${{ needs.process-auto-merge.outputs.action-taken }}" case "$ACTION" in - "enabled") - ACTION_DESC="โœ… Auto-merge enabled" - ;; - "already-enabled") - ACTION_DESC="โœ… Auto-merge already enabled" - ;; - "disabled-changes-requested") - ACTION_DESC="๐Ÿ›‘ Auto-merge disabled (changes requested)" - ;; - "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)" - ;; - "skip-wip") - ACTION_DESC="๐Ÿšง Skipped (work in progress)" - ;; - "skip-missing-automerge-label") - ACTION_DESC="๐Ÿท๏ธ Skipped (missing automerge label)" - ;; - "conditions-not-met") - ACTION_DESC="โณ Conditions not met" - ;; - "failed") - ACTION_DESC="โŒ Failed to enable auto-merge" - ;; - *) - ACTION_DESC="โ“ Unknown action: $ACTION" - ;; + "enabled") ACTION_DESC="โœ… Auto-merge enabled" ;; + "already-enabled") ACTION_DESC="โœ… Auto-merge already enabled" ;; + "disabled-changes-requested") ACTION_DESC="๐Ÿ›‘ Auto-merge disabled (changes requested)" ;; + "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)" ;; + "skip-wip") ACTION_DESC="๐Ÿšง Skipped (work in progress)" ;; + "skip-missing-automerge-label") ACTION_DESC="๐Ÿท๏ธ Skipped (missing automerge label)" ;; + "conditions-not-met") ACTION_DESC="โณ Conditions not met" ;; + "failed") ACTION_DESC="โŒ Failed to enable auto-merge" ;; + *) ACTION_DESC="โ“ Unknown action: $ACTION" ;; esac - echo "## ๐ŸŽฏ Action Taken" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "$ACTION_DESC" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "### ๐Ÿ”ง Current Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Extract configuration for display - MIN_APPROVALS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_MIN_APPROVALS') - REQUIRE_ALL_REVIEWS=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_REQUIRE_ALL_REQUESTED_REVIEWS') - MERGE_TYPES=$(echo "$ENV_JSON" | jq -r '.AUTO_MERGE_ALLOWED_MERGE_TYPES') - 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 - echo "| Min approvals | $MIN_APPROVALS |" >> $GITHUB_STEP_SUMMARY - echo "| Require all reviews | $REQUIRE_ALL_REVIEWS |" >> $GITHUB_STEP_SUMMARY - echo "| Allowed merge types | $MERGE_TYPES |" >> $GITHUB_STEP_SUMMARY - 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 + { + echo "# ๐Ÿค– Auto-merge on Approval Summary" + echo "" + echo "**โฐ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "**๐Ÿ“‹ PR:** #$PR_NUMBER" + echo "" + echo "## ๐ŸŽฏ Action Taken" + echo "" + echo "$ACTION_DESC" + echo "" + echo "### ๐Ÿ”ง Current Configuration" + echo "" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| Min approvals | ${MIN_APPROVALS} |" + echo "| Require all reviews | ${REQUIRE_ALL_REVIEWS} |" + echo "| Allowed merge types | ${MERGE_TYPES} |" + echo "| Skip draft PRs | ${SKIP_DRAFT} |" + echo "| Skip WIP PRs | ${SKIP_WIP} |" + echo "| Skip bot PRs | ${SKIP_BOT_PRS} |" + echo "| Skip fork PRs | ${SKIP_FORK_PRS} |" + echo "" + echo "---" + echo "๐Ÿค– _Automated by GitHub Actions_" + } >> $GITHUB_STEP_SUMMARY # -------------------------------------------------------------------- - # Report final workflow status + # Report final workflow status (stdout) # -------------------------------------------------------------------- - name: ๐Ÿ“ข Report workflow status + if: always() + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + ACTION: ${{ steps.process.outputs.action }} run: | echo "=== ๐Ÿค– Auto-merge on Approval Summary ===" - echo "๐Ÿ“‹ PR: #${{ needs.process-auto-merge.outputs.pr-number }}" + echo "๐Ÿ“‹ PR: #$PR_NUMBER" - ACTION="${{ needs.process-auto-merge.outputs.action-taken }}" case "$ACTION" in - enabled) - echo "โœ… Action: Auto-merge enabled successfully" - ;; - already-enabled) - echo "โœ… Action: Auto-merge was already enabled" - ;; - disabled-changes-requested) - echo "๐Ÿ›‘ Action: Auto-merge disabled due to changes requested" - ;; - skip-fork) - echo "๐Ÿด Action: Skipped - Fork PR (security policy)" - ;; - skip-missing-automerge-label) - echo "๐Ÿท๏ธ Action: Skipped - Missing automerge label" - ;; - skip-*) - echo "โญ๏ธ Action: Skipped - $ACTION" - ;; - conditions-not-met) - echo "โณ Action: Waiting for conditions to be met" - ;; - failed) - echo "โŒ Action: Failed to enable auto-merge" - ;; - *) - echo "โ“ Action: $ACTION" - ;; + enabled) echo "โœ… Action: Auto-merge enabled successfully" ;; + already-enabled) echo "โœ… Action: Auto-merge was already enabled" ;; + disabled-changes-requested) echo "๐Ÿ›‘ Action: Auto-merge disabled due to changes requested" ;; + skip-fork) echo "๐Ÿด Action: Skipped - Fork PR (security policy)" ;; + skip-missing-automerge-label) echo "๐Ÿท๏ธ Action: Skipped - Missing automerge label" ;; + skip-*) echo "โญ๏ธ Action: Skipped - $ACTION" ;; + conditions-not-met) echo "โณ Action: Waiting for conditions to be met" ;; + failed) echo "โŒ Action: Failed to enable auto-merge" ;; + *) echo "โ“ Action: $ACTION" ;; esac echo "๐Ÿ• Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 03ef758..1c08c9a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,6 +25,7 @@ jobs: analyze: name: Analyze runs-on: ubuntu-latest + timeout-minutes: 30 permissions: actions: read @@ -43,6 +44,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 2cf7d22..2da4138 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -25,10 +25,21 @@ name: Dependabot Auto-merge # -------------------------------------------------------------------- # Trigger Configuration +# +# Only `opened` and `synchronize` are listed because: +# - `opened`: first event for any new Dependabot PR. +# - `synchronize`: fires when Dependabot rebases its own PR (e.g., after +# a base-branch update or a conflict). The decision tree +# must re-evaluate so the rebased commit can be auto-merged. +# `reopened` and `ready_for_review` are intentionally omitted โ€” Dependabot +# never reopens closed PRs (it creates new ones) and never opens drafts, +# so those events would only fire for human PRs which the job-level `if:` +# below would skip anyway. Skipping them at the trigger level avoids the +# corresponding skipped runs in the UI. # -------------------------------------------------------------------- on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize] # Security: Restrict default permissions (jobs must explicitly request what they need) permissions: {} @@ -47,24 +58,46 @@ concurrency: jobs: # ---------------------------------------------------------------------------------- - # Load Environment Variables + # Dependabot processing (single consolidated job) + # + # Workflow phases (each preserved as a step): + # ๐ŸŒ Load environment โ†’ reads .github/env/* + # ๐Ÿ”ง Extract configuration โ†’ exports all DEPENDABOT_* vars to env + # ๐Ÿ“Š Fetch metadata โ†’ official Dependabot metadata + # ๐Ÿ“‹ Log dependency details โ†’ trace logging + # ๐Ÿ”’ Check for security โ†’ classify security vs non-security + # ๐ŸŽฏ Determine action โ†’ decision tree over config + metadata + # โš ๏ธ Alert (major/security) โ†’ comment + manual-review label + # ๐Ÿ” Alert (minor prod) โ†’ comment for review + # ๐Ÿš€ Auto-merge approved โ†’ approve + enable auto-merge + # ๐Ÿท๏ธ Add tracking labels โ†’ dependency/update-type labels + # ๐Ÿ“Š Generate summary โ†’ always # ---------------------------------------------------------------------------------- - load-env: - name: ๐ŸŒ Load Environment Variables - runs-on: ubuntu-latest - permissions: - contents: read # Required: Read repository content for sparse checkout + dependabot: + name: ๐Ÿค– Dependabot Auto-merge + runs-on: ubuntu-24.04 + timeout-minutes: 10 # Only run on Dependabot PRs if: github.event.pull_request.user.login == 'dependabot[bot]' - outputs: - env-json: ${{ steps.load-env.outputs.env-json }} + permissions: + contents: write # Required: Enables auto-merge for Dependabot PRs + pull-requests: write # Required: Update and merge Dependabot PRs + issues: write # Required: Comment on related dependency issues + steps: # -------------------------------------------------------------------- # Check out code to access env file # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code (sparse) + # SECURITY: pin the checkout to the trusted base ref so the local + # ./.github/actions/load-env action (run below with contents/pull-requests/issues + # write) can never be a PR-controlled version. Default checkout on pull_request + # events resolves to the PR head. Lower risk here (job is gated to dependabot[bot]), + # but pinned for defense-in-depth and consistency with the other PR workflows. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: ${{ github.base_ref || github.ref }} + persist-credentials: false sparse-checkout: | .github/env .github/actions/load-env @@ -73,84 +106,52 @@ jobs: # Load and parse environment file # -------------------------------------------------------------------- - name: ๐ŸŒ Load environment variables - uses: ./.github/actions/load-env id: load-env + uses: ./.github/actions/load-env - # ---------------------------------------------------------------------------------- - # Process Dependabot PR - # ---------------------------------------------------------------------------------- - process-pr: - name: ๐Ÿค– Process Dependabot PR - needs: [load-env] - runs-on: ubuntu-latest - permissions: - contents: write # Required for Dependabot PRs: Enables auto-merge (not needed for other PRs) - pull-requests: write # Required: Update and merge Dependabot PRs - issues: write # Required: Comment on related dependency issues - outputs: - dependency-names: ${{ steps.metadata.outputs.dependency-names }} - update-type: ${{ steps.metadata.outputs.update-type }} - dependency-type: ${{ steps.metadata.outputs.dependency-type }} - action-taken: ${{ steps.determine-action.outputs.action }} - - steps: # -------------------------------------------------------------------- - # Extract configuration from env-json + # Extract Dependabot configuration up-front # -------------------------------------------------------------------- - name: ๐Ÿ”ง Extract configuration - id: config env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} GH_PAT_TOKEN: ${{ secrets.GH_PAT_TOKEN }} run: | echo "๐Ÿ“‹ Extracting Dependabot configuration from environment..." - # Extract all needed variables - MAINTAINER=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MAINTAINER_USERNAME') - AUTO_MERGE_PATCH=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH') - AUTO_MERGE_MINOR_DEV=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_DEV') - AUTO_MERGE_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_PROD') - AUTO_MERGE_PATCH_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH_INDIRECT') - AUTO_MERGE_MINOR_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_INDIRECT') - AUTO_MERGE_SECURITY=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_SECURITY_NON_MAJOR') - ALERT_ON_MAJOR=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_ALERT_ON_MAJOR') - ALERT_ON_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_ALERT_ON_MINOR_PROD') - MANUAL_REVIEW_LABEL=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MANUAL_REVIEW_LABEL') - AUTO_MERGE_LABELS=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_LABELS') - PREFERRED_TOKEN=$(echo "$ENV_JSON" | jq -r '.PREFERRED_GITHUB_TOKEN') - # Validate required configuration + MAINTAINER=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MAINTAINER_USERNAME') if [[ -z "$MAINTAINER" ]] || [[ "$MAINTAINER" == "null" ]]; then echo "โŒ ERROR: DEPENDABOT_MAINTAINER_USERNAME not set in configuration" >&2 exit 1 fi - # Set as environment variables for all subsequent steps - echo "MAINTAINER=$MAINTAINER" >> $GITHUB_ENV - echo "AUTO_MERGE_PATCH=$AUTO_MERGE_PATCH" >> $GITHUB_ENV - echo "AUTO_MERGE_MINOR_DEV=$AUTO_MERGE_MINOR_DEV" >> $GITHUB_ENV - echo "AUTO_MERGE_MINOR_PROD=$AUTO_MERGE_MINOR_PROD" >> $GITHUB_ENV - echo "AUTO_MERGE_PATCH_INDIRECT=$AUTO_MERGE_PATCH_INDIRECT" >> $GITHUB_ENV - echo "AUTO_MERGE_MINOR_INDIRECT=$AUTO_MERGE_MINOR_INDIRECT" >> $GITHUB_ENV - echo "AUTO_MERGE_SECURITY=$AUTO_MERGE_SECURITY" >> $GITHUB_ENV - echo "ALERT_ON_MAJOR=$ALERT_ON_MAJOR" >> $GITHUB_ENV - echo "ALERT_ON_MINOR_PROD=$ALERT_ON_MINOR_PROD" >> $GITHUB_ENV - echo "MANUAL_REVIEW_LABEL=$MANUAL_REVIEW_LABEL" >> $GITHUB_ENV - echo "AUTO_MERGE_LABELS=$AUTO_MERGE_LABELS" >> $GITHUB_ENV + # Single jq pass for all config we need across steps + { + echo "MAINTAINER=$MAINTAINER" + echo "AUTO_MERGE_PATCH=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH')" + echo "AUTO_MERGE_MINOR_DEV=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_DEV')" + echo "AUTO_MERGE_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_PROD')" + echo "AUTO_MERGE_PATCH_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH_INDIRECT')" + echo "AUTO_MERGE_MINOR_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_INDIRECT')" + echo "AUTO_MERGE_SECURITY=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_SECURITY_NON_MAJOR')" + echo "ALERT_ON_MAJOR=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_ALERT_ON_MAJOR')" + echo "ALERT_ON_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_ALERT_ON_MINOR_PROD')" + echo "MANUAL_REVIEW_LABEL=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MANUAL_REVIEW_LABEL')" + echo "AUTO_MERGE_LABELS=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_LABELS')" + } >> "$GITHUB_ENV" + + PREFERRED_TOKEN=$(echo "$ENV_JSON" | jq -r '.PREFERRED_GITHUB_TOKEN') # Log configuration echo "๐Ÿ” Configuration loaded:" echo " ๐Ÿ‘ค Maintainer: @$MAINTAINER" - echo " ๐Ÿ”ง Auto-merge patch: $AUTO_MERGE_PATCH" - echo " ๐Ÿ”ง Auto-merge minor dev: $AUTO_MERGE_MINOR_DEV" - echo " ๐Ÿ”ง Auto-merge minor prod: $AUTO_MERGE_MINOR_PROD" - echo " ๐Ÿ”ง Auto-merge patch indirect: $AUTO_MERGE_PATCH_INDIRECT" - echo " ๐Ÿ”ง Auto-merge minor indirect: $AUTO_MERGE_MINOR_INDIRECT" - echo " ๐Ÿ”’ Auto-merge security (non-major): $AUTO_MERGE_SECURITY" - echo " โš ๏ธ Alert on major: $ALERT_ON_MAJOR" - echo " ๐Ÿ” Alert on minor prod: $ALERT_ON_MINOR_PROD" - echo " ๐Ÿท๏ธ Manual review label: $MANUAL_REVIEW_LABEL" - echo " ๐Ÿท๏ธ Auto-merge labels: $AUTO_MERGE_LABELS" + echo " ๐Ÿ”ง Auto-merge patch: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH')" + echo " ๐Ÿ”ง Auto-merge minor dev: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_DEV')" + echo " ๐Ÿ”ง Auto-merge minor prod: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_PROD')" + echo " ๐Ÿ”ง Auto-merge patch indirect: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH_INDIRECT')" + echo " ๐Ÿ”ง Auto-merge minor indirect: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_INDIRECT')" + echo " ๐Ÿ”’ Auto-merge security (non-major): $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_SECURITY_NON_MAJOR')" if [[ "$PREFERRED_TOKEN" == "GH_PAT_TOKEN" && -n "$GH_PAT_TOKEN" ]]; then echo " ๐Ÿ”‘ Token: Personal Access Token (PAT)" @@ -748,6 +749,10 @@ jobs: - name: ๐Ÿš€ Auto-merge approved updates if: | startsWith(steps.determine-action.outputs.action, 'auto-merge-') + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} + TOKEN_TYPE: ${{ secrets.GH_PAT_TOKEN && 'PAT' || 'GITHUB_TOKEN' }} run: | echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" echo "๐Ÿš€ EXECUTING AUTO-MERGE" @@ -861,10 +866,6 @@ jobs: # Don't exit with error - the PR is still approved fi - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} - TOKEN_TYPE: ${{ secrets.GH_PAT_TOKEN && 'PAT' || 'GITHUB_TOKEN' }} # -------------------------------------------------------------------- # Add tracking labels @@ -921,137 +922,92 @@ jobs: console.log(`Added labels: ${labels.join(', ')}`); } - # ---------------------------------------------------------------------------------- - # Generate Workflow Summary Report - # ---------------------------------------------------------------------------------- - summary: - name: ๐Ÿ“Š Generate Summary - if: always() && github.event.pull_request.user.login == 'dependabot[bot]' - needs: [load-env, process-pr] - runs-on: ubuntu-latest - steps: # -------------------------------------------------------------------- - # Generate a workflow summary report + # Generate workflow summary report # -------------------------------------------------------------------- - name: ๐Ÿ“Š Generate workflow summary + if: always() env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + PR_NUMBER: ${{ github.event.pull_request.number }} + DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + DEPENDENCY_TYPE: ${{ steps.metadata.outputs.dependency-type }} + ACTION: ${{ steps.determine-action.outputs.action }} run: | echo "๐Ÿ“Š Generating workflow summary..." - echo "# ๐Ÿค– Dependabot Auto-merge Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**โฐ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY - echo "**๐Ÿ“‹ PR:** #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "## ๐Ÿ“ฆ Dependency Information" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| **Dependency** | ${{ needs.process-pr.outputs.dependency-names }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Update Type** | ${{ needs.process-pr.outputs.update-type }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Dependency Type** | ${{ needs.process-pr.outputs.dependency-type }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - # Determine action taken - ACTION="${{ needs.process-pr.outputs.action-taken }}" case "$ACTION" in - "auto-merge-patch") - ACTION_DESC="โœ… Auto-merged (patch update)" - ;; - "auto-merge-patch-indirect") - ACTION_DESC="โœ… Auto-merged (patch update - indirect dependency)" - ;; - "auto-merge-minor-dev") - ACTION_DESC="โœ… Auto-merged (minor dev dependency)" - ;; - "auto-merge-minor-prod") - ACTION_DESC="โœ… Auto-merged (minor prod dependency)" - ;; - "auto-merge-minor-indirect") - ACTION_DESC="โœ… Auto-merged (minor update - indirect dependency)" - ;; - "auto-merge-security") - ACTION_DESC="๐Ÿ”’ Auto-merged (security update)" - ;; - "approved-ready-for-merge") - ACTION_DESC="โœ… Approved and ready for manual merge (auto-merge failed)" - ;; - "alert-major") - ACTION_DESC="โš ๏ธ Manual review required (major update)" - ;; - "alert-security-major") - ACTION_DESC="๐Ÿšจ Manual review required (major security update)" - ;; - "alert-minor-prod") - ACTION_DESC="๐Ÿ” Manual review suggested (minor prod update)" - ;; - "manual-review") - ACTION_DESC="๐Ÿ‘€ Manual review required" - ;; - *) - ACTION_DESC="โ“ Unknown action" - ;; + "auto-merge-patch") ACTION_DESC="โœ… Auto-merged (patch update)" ;; + "auto-merge-patch-indirect") ACTION_DESC="โœ… Auto-merged (patch update - indirect dependency)" ;; + "auto-merge-minor-dev") ACTION_DESC="โœ… Auto-merged (minor dev dependency)" ;; + "auto-merge-minor-prod") ACTION_DESC="โœ… Auto-merged (minor prod dependency)" ;; + "auto-merge-minor-indirect") ACTION_DESC="โœ… Auto-merged (minor update - indirect dependency)" ;; + "auto-merge-security") ACTION_DESC="๐Ÿ”’ Auto-merged (security update)" ;; + "approved-ready-for-merge") ACTION_DESC="โœ… Approved and ready for manual merge (auto-merge failed)" ;; + "alert-major") ACTION_DESC="โš ๏ธ Manual review required (major update)" ;; + "alert-security-major") ACTION_DESC="๐Ÿšจ Manual review required (major security update)" ;; + "alert-minor-prod") ACTION_DESC="๐Ÿ” Manual review suggested (minor prod update)" ;; + "manual-review") ACTION_DESC="๐Ÿ‘€ Manual review required" ;; + *) ACTION_DESC="โ“ Unknown action" ;; esac - echo "## ๐ŸŽฏ Action Taken" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "$ACTION_DESC" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "### ๐Ÿ”ง Current Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Extract configuration for display - MAINTAINER=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MAINTAINER_USERNAME') - AUTO_MERGE_PATCH=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH') - AUTO_MERGE_MINOR_DEV=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_DEV') - AUTO_MERGE_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_PROD') - AUTO_MERGE_PATCH_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH_INDIRECT') - AUTO_MERGE_MINOR_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_INDIRECT') - AUTO_MERGE_SECURITY=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_SECURITY_NON_MAJOR') - - echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY - echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Auto-merge patch | $AUTO_MERGE_PATCH |" >> $GITHUB_STEP_SUMMARY - echo "| Auto-merge minor dev | $AUTO_MERGE_MINOR_DEV |" >> $GITHUB_STEP_SUMMARY - echo "| Auto-merge minor prod | $AUTO_MERGE_MINOR_PROD |" >> $GITHUB_STEP_SUMMARY - echo "| Auto-merge patch indirect | $AUTO_MERGE_PATCH_INDIRECT |" >> $GITHUB_STEP_SUMMARY - echo "| Auto-merge minor indirect | $AUTO_MERGE_MINOR_INDIRECT |" >> $GITHUB_STEP_SUMMARY - echo "| Auto-merge security | $AUTO_MERGE_SECURITY |" >> $GITHUB_STEP_SUMMARY - echo "| Maintainer | @$MAINTAINER |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿค– _Automated by GitHub Actions_" >> $GITHUB_STEP_SUMMARY + { + echo "# ๐Ÿค– Dependabot Auto-merge Summary" + echo "" + echo "**โฐ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "**๐Ÿ“‹ PR:** #$PR_NUMBER" + echo "" + echo "## ๐Ÿ“ฆ Dependency Information" + echo "" + echo "| Property | Value |" + echo "|----------|-------|" + echo "| **Dependency** | ${DEPENDENCY_NAMES} |" + echo "| **Update Type** | ${UPDATE_TYPE} |" + echo "| **Dependency Type** | ${DEPENDENCY_TYPE} |" + echo "" + echo "## ๐ŸŽฏ Action Taken" + echo "" + echo "$ACTION_DESC" + echo "" + echo "### ๐Ÿ”ง Current Configuration" + echo "" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| Auto-merge patch | ${AUTO_MERGE_PATCH} |" + echo "| Auto-merge minor dev | ${AUTO_MERGE_MINOR_DEV} |" + echo "| Auto-merge minor prod | ${AUTO_MERGE_MINOR_PROD} |" + echo "| Auto-merge patch indirect | ${AUTO_MERGE_PATCH_INDIRECT} |" + echo "| Auto-merge minor indirect | ${AUTO_MERGE_MINOR_INDIRECT} |" + echo "| Auto-merge security | ${AUTO_MERGE_SECURITY} |" + echo "| Maintainer | @${MAINTAINER} |" + echo "" + echo "---" + echo "๐Ÿค– _Automated by GitHub Actions_" + } >> $GITHUB_STEP_SUMMARY # -------------------------------------------------------------------- - # Report final workflow status + # Report final workflow status (stdout) # -------------------------------------------------------------------- - name: ๐Ÿ“ข Report workflow status + if: always() + env: + DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + DEPENDENCY_TYPE: ${{ steps.metadata.outputs.dependency-type }} + ACTION: ${{ steps.determine-action.outputs.action }} run: | echo "=== ๐Ÿค– Dependabot Auto-merge Summary ===" - echo "๐Ÿ“ฆ Dependency: ${{ needs.process-pr.outputs.dependency-names }}" - echo "๐Ÿ”„ Update type: ${{ needs.process-pr.outputs.update-type }}" - echo "๐Ÿ“ Dependency type: ${{ needs.process-pr.outputs.dependency-type }}" + echo "๐Ÿ“ฆ Dependency: $DEPENDENCY_NAMES" + echo "๐Ÿ”„ Update type: $UPDATE_TYPE" + echo "๐Ÿ“ Dependency type: $DEPENDENCY_TYPE" - ACTION="${{ needs.process-pr.outputs.action-taken }}" case "$ACTION" in - auto-merge-*) - echo "โœ… Action: Auto-merge enabled" - ;; - approved-ready-for-merge) - echo "โœ… Action: Approved and ready for manual merge" - ;; - alert-*) - echo "โš ๏ธ Action: Alert sent, manual review required" - ;; - manual-review) - echo "๐Ÿ‘€ Action: Manual review required" - ;; - *) - echo "โ“ Action: $ACTION" - ;; + auto-merge-*) echo "โœ… Action: Auto-merge enabled" ;; + approved-ready-for-merge) echo "โœ… Action: Approved and ready for manual merge" ;; + alert-*) echo "โš ๏ธ Action: Alert sent, manual review required" ;; + manual-review) echo "๐Ÿ‘€ Action: Manual review required" ;; + *) echo "โ“ Action: $ACTION" ;; esac echo "๐Ÿ• Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" diff --git a/.github/workflows/fortress-benchmarks.yml b/.github/workflows/fortress-benchmarks.yml index db91c7a..3019476 100644 --- a/.github/workflows/fortress-benchmarks.yml +++ b/.github/workflows/fortress-benchmarks.yml @@ -130,6 +130,8 @@ jobs: # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # -------------------------------------------------------------------- # Setup Go with caching and version management diff --git a/.github/workflows/fortress-code-quality.yml b/.github/workflows/fortress-code-quality.yml index 97ae533..92fd5b6 100644 --- a/.github/workflows/fortress-code-quality.yml +++ b/.github/workflows/fortress-code-quality.yml @@ -1,8 +1,8 @@ # ------------------------------------------------------------------------------------ # Code Quality (Reusable Workflow) (GoFortress) # -# Purpose: Run code quality checks including Go vet (static analysis) and -# golangci-lint (comprehensive linting). +# Purpose: Run code quality checks including Go vet (static analysis), YAML/JSON +# format validation, and golangci-lint (comprehensive linting). # # Maintainer: @mrz1836 # @@ -47,7 +47,7 @@ on: value: ${{ jobs.lint.outputs.golangci-lint-version }} yamlfmt-version: description: "Version of yamlfmt used in the workflow" - value: ${{ jobs.yaml-format.outputs.yamlfmt-version }} + value: ${{ jobs.static-checks.outputs.yamlfmt-version }} secrets: github-token: description: "GitHub token for API access" @@ -58,34 +58,39 @@ permissions: {} jobs: # ---------------------------------------------------------------------------------- - # Go Vet (Static Analysis) + # Static Checks (govet + YAML/JSON format) + # + # govet and yaml-format share a single checkout โ†’ parse env โ†’ setup Go โ†’ extract + # module dir โ†’ setup MAGE-X pass. Each scan keeps its own `if:` gate, + # `continue-on-error: true` semantics, summary, and artifact upload. A final + # aggregation step exits non-zero if either scan failed, preserving job-failure + # semantics. # ---------------------------------------------------------------------------------- - govet: - name: ๐Ÿ“Š Govet (Static Analysis) - if: ${{ inputs.static-analysis-enabled == 'true' }} + static-checks: + name: ๐Ÿ“Š Static Checks (Govet + YAML Format) + if: ${{ inputs.static-analysis-enabled == 'true' || inputs.yaml-lint-enabled == 'true' }} runs-on: ${{ inputs.primary-runner }} + timeout-minutes: 10 permissions: contents: read + outputs: + yamlfmt-version: ${{ steps.yamlfmt-version.outputs.version }} steps: # -------------------------------------------------------------------- - # Checkout code (required for local actions) + # Shared setup # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - name: ๐Ÿ”ง Parse environment variables uses: ./.github/actions/parse-env with: env-json: ${{ inputs.env-json }} - # -------------------------------------------------------------------- - # Setup Go with caching and version management - # -------------------------------------------------------------------- - name: ๐Ÿ—๏ธ Setup Go with Cache - id: setup-go-vet + id: setup-go-static uses: ./.github/actions/setup-go-with-cache with: go-version: ${{ inputs.go-primary-version }} @@ -96,17 +101,11 @@ jobs: enable-multi-module: ${{ env.ENABLE_MULTI_MODULE_TESTING }} github-token: ${{ secrets.github-token }} - # -------------------------------------------------------------------- - # Extract Go module directory from GO_SUM_FILE path - # -------------------------------------------------------------------- - name: ๐Ÿ”ง Extract Go module directory uses: ./.github/actions/extract-module-dir with: go-sum-file: ${{ env.GO_SUM_FILE }} - # -------------------------------------------------------------------- - # Setup MAGE-X (required for magex lint command) - # -------------------------------------------------------------------- - name: ๐Ÿ”ง Setup MAGE-X uses: ./.github/actions/setup-magex with: @@ -114,18 +113,18 @@ jobs: runner-os: ${{ inputs.primary-runner }} use-local: ${{ env.MAGE_X_USE_LOCAL }} - # -------------------------------------------------------------------- - # Run go vet with sequential execution to avoid memory issues - # -------------------------------------------------------------------- + # ==================================================================== + # GO VET (static analysis) + # ==================================================================== - name: ๐Ÿ” Go vet (sequential) id: run-govet + if: inputs.static-analysis-enabled == 'true' continue-on-error: true run: | echo "๐Ÿš€ Running static analysis with go vet (sequential mode)..." GO_MODULE_DIR="${{ env.GO_MODULE_DIR }}" GOVET_EXIT_CODE=0 - # Run go vet on packages sequentially to reduce memory usage if [ -n "$GO_MODULE_DIR" ]; then echo "๐Ÿ”ง Running go vet from directory: $GO_MODULE_DIR" cd "$GO_MODULE_DIR" @@ -134,10 +133,7 @@ jobs: fi # Get all packages and vet them one at a time - # Capture go list output and check for errors - # Discard stderr to avoid capturing download progress messages if ! PACKAGES=$(go list ./... 2>/dev/null | grep -v /vendor/); then - # If command failed, re-run with stderr visible to show the error echo "โŒ go list command failed:" | tee govet-output.log go list ./... 2>&1 | head -20 | tee -a govet-output.log echo "govet-exit-code=1" >> $GITHUB_OUTPUT @@ -155,7 +151,7 @@ jobs: echo "โš ๏ธ No packages found to vet" | tee -a govet-output.log echo "govet-exit-code=1" >> $GITHUB_OUTPUT echo "govet-status=failure" >> $GITHUB_OUTPUT - exit 0 # Continue to allow summary generation + exit 0 fi for pkg in $PACKAGES; do @@ -176,57 +172,55 @@ jobs: echo "โŒ Static analysis completed with errors" fi - # -------------------------------------------------------------------- - # Create GitHub Annotations for failures - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations + - name: ๐Ÿ“‹ Govet โ€” Create GitHub Annotations if: always() && steps.run-govet.outputs.govet-status == 'failure' run: | echo "::error title=Go Vet Failed::Static analysis issues detected - see job summary for details" - # -------------------------------------------------------------------- - # Summary of Go vet results - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Job Summary - if: always() + - name: ๐Ÿ“Š Govet โ€” Job Summary + if: always() && inputs.static-analysis-enabled == 'true' run: | GOVET_STATUS="${{ steps.run-govet.outputs.govet-status }}" - echo "## ๐Ÿ“Š Go Vet Static Analysis Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Analysis Details | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Tool** | go vet (Official Go Static Analyzer) |" >> $GITHUB_STEP_SUMMARY - echo "| **Execution** | Sequential (memory-optimized) |" >> $GITHUB_STEP_SUMMARY - echo "| **Scope** | ./... (excludes dependencies) |" >> $GITHUB_STEP_SUMMARY + { + echo "## ๐Ÿ“Š Go Vet Static Analysis Summary" + echo "" + echo "| Analysis Details | Status |" + echo "|---|---|" + echo "| **Tool** | go vet (Official Go Static Analyzer) |" + echo "| **Execution** | Sequential (memory-optimized) |" + echo "| **Scope** | ./... (excludes dependencies) |" + } >> $GITHUB_STEP_SUMMARY if [[ "$GOVET_STATUS" == "success" ]]; then - echo "| **Result** | โœ… No issues found |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐ŸŽฏ **All packages passed static analysis checks.**" >> $GITHUB_STEP_SUMMARY + { + echo "| **Result** | โœ… No issues found |" + echo "" + echo "๐ŸŽฏ **All packages passed static analysis checks.**" + } >> $GITHUB_STEP_SUMMARY else - echo "| **Result** | โŒ Issues detected |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + { + echo "| **Result** | โŒ Issues detected |" + echo "" + } >> $GITHUB_STEP_SUMMARY - # Show failure details if applicable if [[ -f govet-output.log ]]; then - echo "### ๐Ÿšจ Error Details" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand full output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -200 govet-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY + { + echo "### ๐Ÿšจ Error Details" + echo "" + echo "
" + echo "Click to expand full output" + echo "" + echo '```' + head -200 govet-output.log + echo '```' + echo "
" + } >> $GITHUB_STEP_SUMMARY fi fi - # -------------------------------------------------------------------- - # Upload go vet results - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload go vet results - if: always() + - name: ๐Ÿ“ค Govet โ€” Upload results + if: always() && inputs.static-analysis-enabled == 'true' uses: ./.github/actions/upload-artifact-resilient with: artifact-name: govet-results @@ -234,44 +228,212 @@ jobs: retention-days: "7" if-no-files-found: ignore - # -------------------------------------------------------------------- - # Collect cache statistics - # -------------------------------------------------------------------- + # ==================================================================== + # YAML/JSON FORMAT VALIDATION + # ==================================================================== + - name: ๐Ÿ” YAML โ€” Get yamlfmt version + id: yamlfmt-version + if: inputs.yaml-lint-enabled == 'true' + run: | + echo "โœ… Using MAGE-X managed yamlfmt version" + echo "version=${{ env.MAGE_X_YAMLFMT_VERSION }}" >> $GITHUB_OUTPUT + + - name: ๐Ÿ“‹ YAML โ€” List YAML/JSON files to check + if: inputs.yaml-lint-enabled == 'true' + run: | + echo "๐Ÿ“Š YAML/JSON files that will be validated:" + find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.json" \) \ + -not -path "./.git/*" \ + -not -path "./vendor/*" \ + -not -path "./node_modules/*" \ + -not -path "./dist/*" \ + -not -path "./build/*" \ + -not -path "./coverage/*" | sort || true + + TOTAL_FILES=$(find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.json" \) \ + -not -path "./.git/*" \ + -not -path "./vendor/*" \ + -not -path "./node_modules/*" \ + -not -path "./dist/*" \ + -not -path "./build/*" \ + -not -path "./coverage/*" | wc -l | xargs) + + echo "TOTAL_FILES=$TOTAL_FILES" >> $GITHUB_ENV + echo "" + echo "๐Ÿ“ˆ Total YAML/JSON files found: $TOTAL_FILES" + + - name: ๐ŸŽจ YAML โ€” Check formatting with MAGE-X + id: run-format-check + if: inputs.yaml-lint-enabled == 'true' + continue-on-error: true + run: | + echo "๐Ÿ” Checking YAML/JSON file formatting with MAGE-X format:check..." + echo "๐Ÿ“„ Configuration file: .github/.yamlfmt" + echo "๐Ÿ”ง yamlfmt version: ${{ steps.yamlfmt-version.outputs.version }}" + echo "" + + GO_MODULE_DIR="${{ env.GO_MODULE_DIR }}" + + set +e + if [ -n "$GO_MODULE_DIR" ]; then + echo "๐Ÿ”ง Running magex format:check from directory: $GO_MODULE_DIR" + (cd "$GO_MODULE_DIR" && magex format:check 2>&1 | tee ../format-output.log) + FORMAT_EXIT_CODE=${PIPESTATUS[0]} + else + echo "๐Ÿ”ง Running magex format:check from repository root" + magex format:check 2>&1 | tee format-output.log + FORMAT_EXIT_CODE=${PIPESTATUS[0]} + fi + set -e + + echo "format-exit-code=$FORMAT_EXIT_CODE" >> $GITHUB_OUTPUT + if [[ $FORMAT_EXIT_CODE -eq 0 ]]; then + echo "format-status=success" >> $GITHUB_OUTPUT + echo "โœ… All YAML/JSON files are properly formatted" + else + echo "format-status=failure" >> $GITHUB_OUTPUT + echo "โŒ YAML/JSON formatting issues detected" + echo "" + echo "๐Ÿ”ง To fix these issues locally, run:" + if [ -n "$GO_MODULE_DIR" ]; then + echo " cd $GO_MODULE_DIR && magex format:fix" + else + echo " magex format:fix" + fi + fi + + - name: ๐Ÿ“‹ YAML โ€” Create GitHub Annotations + if: always() && steps.run-format-check.outputs.format-status == 'failure' + run: | + echo "::error title=YAML/JSON Format Check Failed::Formatting issues detected - see job summary for details" + + - name: ๐Ÿ“Š YAML โ€” Job Summary + if: always() && inputs.yaml-lint-enabled == 'true' + run: | + FORMAT_STATUS="${{ steps.run-format-check.outputs.format-status }}" + + { + echo "## ๐Ÿ“ YAML/JSON Format Validation Summary" + echo "" + echo "| Validation Details | Status |" + echo "|---|---|" + echo "| **Tool** | MAGE-X format:fix (yamlfmt) |" + echo "| **Version** | ${{ steps.yamlfmt-version.outputs.version }} |" + echo "| **Configuration** | .github/.yamlfmt |" + echo "| **Scope** | All .yml, .yaml, and .json files |" + } >> $GITHUB_STEP_SUMMARY + + if [[ "$FORMAT_STATUS" == "success" ]]; then + echo "| **Result** | โœ… All files properly formatted |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Result** | โŒ Formatting issues detected |" >> $GITHUB_STEP_SUMMARY + fi + + { + echo "" + echo "### File Processing Statistics" + echo "- **Total files processed**: ${{ env.TOTAL_FILES }}" + echo "" + echo "### yamlfmt Configuration Applied" + echo "- **Indent Style**: Spaces (2 spaces)" + echo "- **Line Endings**: LF" + echo "- **Final Newline**: Required" + echo "- **Line Breaks**: Preserved where sensible" + echo "- **Comment Padding**: 1 space after #" + echo "" + } >> $GITHUB_STEP_SUMMARY + + if [[ "$FORMAT_STATUS" != "success" ]] && [[ -f format-output.log ]]; then + { + echo "### ๐Ÿšจ Formatting Issues" + echo "" + echo "
" + echo "Click to expand full output" + echo "" + echo '```' + head -200 format-output.log + echo '```' + echo "
" + echo "" + echo "๐Ÿ”ง **To fix these issues locally, run:** \`magex format:fix\`" + } >> $GITHUB_STEP_SUMMARY + else + echo "๐ŸŽฏ **All YAML/JSON files meet formatting standards via MAGE-X.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: ๐Ÿ“ค YAML โ€” Upload format check results + if: always() && inputs.yaml-lint-enabled == 'true' + uses: ./.github/actions/upload-artifact-resilient + with: + artifact-name: format-check-results + artifact-path: format-output.log + retention-days: "7" + if-no-files-found: ignore + + # ==================================================================== + # SHARED: cache statistics + final failure aggregation + # ==================================================================== - name: ๐Ÿ“Š Collect cache statistics - id: cache-stats-govet + id: cache-stats-static if: always() uses: ./.github/actions/collect-cache-stats with: - workflow-name: govet - job-name: govet + workflow-name: static-checks + job-name: static-checks os: ${{ inputs.primary-runner }} go-version: ${{ inputs.go-primary-version }} cache-prefix: cache-stats - gomod-cache-hit: ${{ steps.setup-go-vet.outputs.module-cache-hit }} - gobuild-cache-hit: ${{ steps.setup-go-vet.outputs.build-cache-hit }} + gomod-cache-hit: ${{ steps.setup-go-static.outputs.module-cache-hit }} + gobuild-cache-hit: ${{ steps.setup-go-static.outputs.build-cache-hit }} - # -------------------------------------------------------------------- - # Upload infrastructure cache statistics - # -------------------------------------------------------------------- - name: ๐Ÿ“ค Upload infrastructure cache statistics if: always() uses: ./.github/actions/upload-statistics with: - artifact-name: cache-stats-govet - artifact-path: cache-stats-govet.json + artifact-name: cache-stats-static-checks + artifact-path: cache-stats-static-checks.json retention-days: 1 # -------------------------------------------------------------------- - # Fail job if issues found + # Aggregate failures: exit non-zero if EITHER scan failed. + # Preserves the per-scan isolation (each ran with continue-on-error) + # while still failing the job, which the parent workflow sees as + # a "code-quality" job failure. # -------------------------------------------------------------------- - - name: ๐Ÿšจ Fail job if issues found - if: always() && steps.run-govet.outputs.govet-status == 'failure' + - name: ๐Ÿšจ Aggregate failures + if: always() + # Security: workflow_call inputs are routed through `env:` rather than + # interpolated into the shell body. This prevents script injection + # (SonarCloud githubactions script-injection rule) by ensuring the + # values are passed as environment data, never spliced into the script. + env: + STATIC_ANALYSIS_ENABLED: ${{ inputs.static-analysis-enabled }} + YAML_LINT_ENABLED: ${{ inputs.yaml-lint-enabled }} + GOVET_STATUS: ${{ steps.run-govet.outputs.govet-status }} + FORMAT_STATUS: ${{ steps.run-format-check.outputs.format-status }} run: | - echo "โŒ Go vet detected static analysis issues" - exit 1 + FAILED=0 + + if [[ "${STATIC_ANALYSIS_ENABLED}" == "true" ]] && [[ "${GOVET_STATUS}" == "failure" ]]; then + echo "โŒ go vet detected static analysis issues" + FAILED=1 + fi + + if [[ "${YAML_LINT_ENABLED}" == "true" ]] && [[ "${FORMAT_STATUS}" == "failure" ]]; then + echo "โŒ YAML/JSON format check detected formatting issues" + FAILED=1 + fi + + if [[ $FAILED -ne 0 ]]; then + exit 1 + fi + + echo "โœ… All enabled static checks passed" # ---------------------------------------------------------------------------------- # Lint (Code Linting) + # + # Kept as its own job so golangci-lint can run in parallel with the other checks. # ---------------------------------------------------------------------------------- lint: name: โœจ Lint Code @@ -288,6 +450,8 @@ jobs: # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # -------------------------------------------------------------------- # Parse environment variables @@ -558,228 +722,3 @@ jobs: run: | echo "โŒ Lint detected code quality issues" exit 1 - - # ---------------------------------------------------------------------------------- - # YAML/JSON Format Validation (MAGE-X) - # ---------------------------------------------------------------------------------- - yaml-format: - name: ๐Ÿ“ YAML/JSON Format Validation - if: ${{ inputs.yaml-lint-enabled == 'true' }} - runs-on: ${{ inputs.primary-runner }} - permissions: - contents: read - outputs: - yamlfmt-version: ${{ steps.yamlfmt-version.outputs.version }} - steps: - # -------------------------------------------------------------------- - # Checkout code (required for local actions) - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - uses: ./.github/actions/parse-env - with: - env-json: ${{ inputs.env-json }} - - # -------------------------------------------------------------------- - # Setup Go with caching and version management - # -------------------------------------------------------------------- - - name: ๐Ÿ—๏ธ Setup Go with Cache - id: setup-go-yaml - uses: ./.github/actions/setup-go-with-cache - with: - go-version: ${{ inputs.go-primary-version }} - matrix-os: ${{ inputs.primary-runner }} - go-primary-version: ${{ inputs.go-primary-version }} - go-secondary-version: ${{ inputs.go-primary-version }} - go-sum-file: ${{ env.GO_SUM_FILE }} - enable-multi-module: ${{ env.ENABLE_MULTI_MODULE_TESTING }} - github-token: ${{ secrets.github-token }} - - # -------------------------------------------------------------------- - # Extract Go module directory from GO_SUM_FILE path - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Extract Go module directory - uses: ./.github/actions/extract-module-dir - with: - go-sum-file: ${{ env.GO_SUM_FILE }} - - # -------------------------------------------------------------------- - # Setup MAGE-X (required for magex format:fix command) - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Setup MAGE-X - uses: ./.github/actions/setup-magex - with: - magex-version: ${{ env.MAGE_X_VERSION }} - runner-os: ${{ inputs.primary-runner }} - use-local: ${{ env.MAGE_X_USE_LOCAL }} - - # -------------------------------------------------------------------- - # Get yamlfmt version from MAGE-X - # -------------------------------------------------------------------- - - name: ๐Ÿ” Get yamlfmt version - id: yamlfmt-version - run: | - echo "โœ… Using MAGE-X managed yamlfmt version" - echo "version=${{ env.MAGE_X_YAMLFMT_VERSION }}" >> $GITHUB_OUTPUT - - # -------------------------------------------------------------------- - # List YAML/JSON files to be formatted (for transparency) - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ List YAML/JSON files to check - run: | - echo "๐Ÿ“Š YAML/JSON files that will be validated:" - find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.json" \) \ - -not -path "./.git/*" \ - -not -path "./vendor/*" \ - -not -path "./node_modules/*" \ - -not -path "./dist/*" \ - -not -path "./build/*" \ - -not -path "./coverage/*" | sort || true - - TOTAL_FILES=$(find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.json" \) \ - -not -path "./.git/*" \ - -not -path "./vendor/*" \ - -not -path "./node_modules/*" \ - -not -path "./dist/*" \ - -not -path "./build/*" \ - -not -path "./coverage/*" | wc -l | xargs) - - echo "TOTAL_FILES=$TOTAL_FILES" >> $GITHUB_ENV - echo "" - echo "๐Ÿ“ˆ Total YAML/JSON files found: $TOTAL_FILES" - - # -------------------------------------------------------------------- - # Run MAGE-X format:check to validate formatting - # -------------------------------------------------------------------- - - name: ๐Ÿ” Check YAML/JSON formatting with MAGE-X - id: run-format-check - continue-on-error: true - run: | - echo "๐Ÿ” Checking YAML/JSON file formatting with MAGE-X format:check..." - echo "๐Ÿ“„ Configuration file: .github/.yamlfmt" - echo "๐Ÿ”ง yamlfmt version: ${{ steps.yamlfmt-version.outputs.version }}" - echo "" - - GO_MODULE_DIR="${{ env.GO_MODULE_DIR }}" - - set +e - # Run magex format:check to validate formatting without modifying files - if [ -n "$GO_MODULE_DIR" ]; then - echo "๐Ÿ”ง Running magex format:check from directory: $GO_MODULE_DIR" - (cd "$GO_MODULE_DIR" && magex format:check 2>&1 | tee ../format-output.log) - FORMAT_EXIT_CODE=${PIPESTATUS[0]} - else - echo "๐Ÿ”ง Running magex format:check from repository root" - magex format:check 2>&1 | tee format-output.log - FORMAT_EXIT_CODE=${PIPESTATUS[0]} - fi - set -e - - echo "format-exit-code=$FORMAT_EXIT_CODE" >> $GITHUB_OUTPUT - if [[ $FORMAT_EXIT_CODE -eq 0 ]]; then - echo "format-status=success" >> $GITHUB_OUTPUT - echo "โœ… All YAML/JSON files are properly formatted" - else - echo "format-status=failure" >> $GITHUB_OUTPUT - echo "โŒ YAML/JSON formatting issues detected" - echo "" - echo "๐Ÿ”ง To fix these issues locally, run:" - if [ -n "$GO_MODULE_DIR" ]; then - echo " cd $GO_MODULE_DIR && magex format:fix" - else - echo " magex format:fix" - fi - echo "" - echo "๐Ÿ“š yamlfmt Configuration (.github/.yamlfmt):" - echo " โ€ข Indent style: spaces (2 spaces)" - echo " โ€ข End of line: LF" - echo " โ€ข Final newline: required" - echo " โ€ข Line breaks: preserved where sensible" - echo " โ€ข Comment padding: 1 space after #" - fi - - # -------------------------------------------------------------------- - # Create GitHub Annotations for failures - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations - if: always() && steps.run-format-check.outputs.format-status == 'failure' - run: | - echo "::error title=YAML/JSON Format Check Failed::Formatting issues detected - see job summary for details" - - # -------------------------------------------------------------------- - # Job Summary - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Job Summary - if: always() - run: | - FORMAT_STATUS="${{ steps.run-format-check.outputs.format-status }}" - - echo "## ๐Ÿ“ YAML/JSON Format Validation Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Validation Details | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Tool** | MAGE-X format:fix (yamlfmt) |" >> $GITHUB_STEP_SUMMARY - echo "| **Version** | ${{ steps.yamlfmt-version.outputs.version }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Configuration** | .github/.yamlfmt |" >> $GITHUB_STEP_SUMMARY - echo "| **Scope** | All .yml, .yaml, and .json files |" >> $GITHUB_STEP_SUMMARY - - if [[ "$FORMAT_STATUS" == "success" ]]; then - echo "| **Result** | โœ… All files properly formatted |" >> $GITHUB_STEP_SUMMARY - else - echo "| **Result** | โŒ Formatting issues detected |" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### File Processing Statistics" >> $GITHUB_STEP_SUMMARY - echo "- **Total files processed**: ${{ env.TOTAL_FILES }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### yamlfmt Configuration Applied" >> $GITHUB_STEP_SUMMARY - echo "- **Indent Style**: Spaces (2 spaces)" >> $GITHUB_STEP_SUMMARY - echo "- **Line Endings**: LF" >> $GITHUB_STEP_SUMMARY - echo "- **Final Newline**: Required" >> $GITHUB_STEP_SUMMARY - echo "- **Line Breaks**: Preserved where sensible" >> $GITHUB_STEP_SUMMARY - echo "- **Comment Padding**: 1 space after #" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Show failure details if applicable - if [[ "$FORMAT_STATUS" != "success" ]] && [[ -f format-output.log ]]; then - echo "### ๐Ÿšจ Formatting Issues" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand full output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -200 format-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿ”ง **To fix these issues locally, run:** \`magex format:fix\`" >> $GITHUB_STEP_SUMMARY - else - echo "๐ŸŽฏ **All YAML/JSON files meet formatting standards via MAGE-X.**" >> $GITHUB_STEP_SUMMARY - fi - - # -------------------------------------------------------------------- - # Upload format check results - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload format check results - if: always() - uses: ./.github/actions/upload-artifact-resilient - with: - artifact-name: format-check-results - artifact-path: format-output.log - retention-days: "7" - if-no-files-found: ignore - - # -------------------------------------------------------------------- - # Fail job if formatting issues found - # -------------------------------------------------------------------- - - name: ๐Ÿšจ Fail job if formatting issues found - if: always() && steps.run-format-check.outputs.format-status == 'failure' - run: | - echo "โŒ Format check detected YAML/JSON formatting issues" - exit 1 diff --git a/.github/workflows/fortress-completion-finalize.yml b/.github/workflows/fortress-completion-finalize.yml deleted file mode 100644 index 82c26c8..0000000 --- a/.github/workflows/fortress-completion-finalize.yml +++ /dev/null @@ -1,381 +0,0 @@ -# ------------------------------------------------------------------------------------ -# Completion Report Finalization (Reusable Workflow) (GoFortress) -# -# Purpose: Finalize the completion report by generating job summaries, performance -# insights, and assembling all report sections into the final published report. -# -# This workflow handles: -# - Job results summary with status indicators -# - Performance insights and workflow analytics -# - Report assembly from all sub-workflow sections -# - Final publication to GitHub Step Summary -# -# Maintainer: @mrz1836 -# -# ------------------------------------------------------------------------------------ - -name: GoFortress (Completion Finalize) - -on: - workflow_call: - inputs: - all-inputs: - description: "JSON string of all original workflow inputs" - required: true - type: string - statistics-report: - description: "Statistics section markdown content" - required: true - type: string - tests-report: - description: "Tests section markdown content" - required: true - type: string - timing-data: - description: "JSON string of timing metrics" - required: true - type: string - outputs: - final-report: - description: "Complete assembled report" - value: ${{ jobs.finalize-report.outputs.report-content }} - -# Security: Restrict default permissions (jobs must explicitly request what they need) -permissions: {} - -jobs: - # ---------------------------------------------------------------------------------- - # Report Finalization - # ---------------------------------------------------------------------------------- - finalize-report: - name: โœ… Finalize Report - runs-on: ubuntu-latest - if: always() - permissions: - contents: read - actions: read - outputs: - report-content: ${{ steps.set-output.outputs.content }} - steps: - # -------------------------------------------------------------------- - # Checkout repository for local actions - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # -------------------------------------------------------------------- - # Parse inputs and setup - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse workflow inputs - env: - ALL_INPUTS: ${{ inputs.all-inputs }} - TIMING_DATA: ${{ inputs.timing-data }} - run: | - echo "๐Ÿ“‹ Parsing workflow inputs..." - # Note: Replace hyphens with underscores in keys for GitHub Actions expression compatibility - # Use heredoc syntax to safely handle multiline values (e.g., env-json) - echo "$ALL_INPUTS" | jq -r 'to_entries | .[] | @base64' | while read -r entry; do - decoded=$(echo "$entry" | base64 -d) - key=$(echo "$decoded" | jq -r '.key') - value=$(echo "$decoded" | jq -r '.value') - normalized_key=$(echo "$key" | tr '-' '_') - { - echo "INPUT_$normalized_key<> $GITHUB_ENV - done - - echo "๐Ÿ“‹ Parsing timing data..." - echo "$TIMING_DATA" | jq -r 'to_entries | .[] | @base64' | while read -r entry; do - decoded=$(echo "$entry" | base64 -d) - key=$(echo "$decoded" | jq -r '.key') - value=$(echo "$decoded" | jq -r '.value') - normalized_key=$(echo "$key" | tr '-' '_') - { - echo "TIMING_$normalized_key<> $GITHUB_ENV - done - - # -------------------------------------------------------------------- - # Download report sections from sub-workflows - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Download statistics section - if: always() - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "statistics-section" - path: ./sections/ - merge-multiple: false - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ“ฅ Download tests section - if: always() - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "tests-section" - path: ./sections/ - merge-multiple: false - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - # -------------------------------------------------------------------- - # Initialize final report with STATUS BANNER FIRST - # -------------------------------------------------------------------- - - name: ๐Ÿ“ Initialize Final Report - run: | - # Determine overall workflow status - WORKFLOW_FAILED=false - FAILED_JOBS="" - - # Check each critical job result - if [[ "${{ env.INPUT_setup_result }}" != "success" && "${{ env.INPUT_setup_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Setup Configuration\n" - fi - if [[ "${{ env.INPUT_test_magex_result }}" != "success" && "${{ env.INPUT_test_magex_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Test MAGE-X\n" - fi - if [[ "${{ env.INPUT_pre_commit_result }}" != "success" && "${{ env.INPUT_pre_commit_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Pre-commit Checks\n" - fi - if [[ "${{ env.INPUT_security_result }}" != "success" && "${{ env.INPUT_security_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Security Scans\n" - fi - if [[ "${{ env.INPUT_code_quality_result }}" != "success" && "${{ env.INPUT_code_quality_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Code Quality\n" - fi - if [[ "${{ env.INPUT_test_suite_result }}" != "success" && "${{ env.INPUT_test_suite_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Test Suite\n" - fi - if [[ "${{ env.INPUT_benchmarks_result }}" != "success" && "${{ env.INPUT_benchmarks_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Benchmarks\n" - fi - if [[ "${{ env.INPUT_release_result }}" != "success" && "${{ env.INPUT_release_result }}" != "skipped" ]]; then - WORKFLOW_FAILED=true - FAILED_JOBS="$FAILED_JOBS- โŒ Release\n" - fi - - SUMMARY_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - - { - # ================================================================= - # STATUS BANNER (Always visible at top - immediate failure visibility) - # ================================================================= - echo "# ๐Ÿ Workflow Complete" - echo "" - - if [[ "$WORKFLOW_FAILED" == "true" ]]; then - echo "> [!CAUTION]" - echo "> ## ๐Ÿ”ด WORKFLOW FAILED" - echo ">" - echo "> **Failed Jobs:**" - echo -e "$FAILED_JOBS" | while IFS= read -r line; do echo "> $line"; done - echo "" - else - echo "> [!TIP]" - echo "> ## ๐ŸŸข ALL CHECKS PASSED" - fi - echo "" - echo "| Job | Result |" - echo "|-----|--------|" - echo "| Setup Configuration | $([ "${{ env.INPUT_setup_result }}" = "success" ] && echo "โœ… Passed" || ([ "${{ env.INPUT_setup_result }}" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" - echo "| Test MAGE-X | $([ "${{ env.INPUT_test_magex_result }}" = "success" ] && echo "โœ… Passed" || ([ "${{ env.INPUT_test_magex_result }}" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" - echo "| Pre-commit Checks | $([ "${{ env.INPUT_pre_commit_result }}" = "success" ] && echo "โœ… Passed" || ([ "${{ env.INPUT_pre_commit_result }}" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" - echo "| Security Scans | $([ "${{ env.INPUT_security_result }}" = "success" ] && echo "โœ… Passed" || ([ "${{ env.INPUT_security_result }}" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" - echo "| Code Quality | $([ "${{ env.INPUT_code_quality_result }}" = "success" ] && echo "โœ… Passed" || ([ "${{ env.INPUT_code_quality_result }}" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" - echo "| Test Suite | $([ "${{ env.INPUT_test_suite_result }}" = "success" ] && echo "โœ… Passed" || ([ "${{ env.INPUT_test_suite_result }}" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" - # Only show benchmarks if attempted - if [[ "${{ env.INPUT_benchmarks_result }}" != "skipped" ]]; then - echo "| Benchmarks | $([ "${{ env.INPUT_benchmarks_result }}" = "success" ] && echo "โœ… Passed" || echo "โŒ Failed") |" - fi - # Only show release if attempted - if [[ "${{ env.INPUT_release_result }}" != "skipped" ]]; then - echo "| Release | $([ "${{ env.INPUT_release_result }}" = "success" ] && echo "โœ… Passed" || echo "โŒ Failed") |" - fi - echo "" - echo "**Duration:** ${TIMING_total_minutes:-0}m ${TIMING_total_seconds:-0}s" - echo "" - echo "**Generated:** $SUMMARY_TIME" - echo "" - - # ================================================================= - # DETAILED SECTIONS (Collapsed by default) - # ================================================================= - echo "
" - echo "๐Ÿ“Š Statistics (Cache, Coverage, LOC)" - echo "" - } > final-report.md - - # -------------------------------------------------------------------- - # Append report sections from sub-workflows (inside collapsed details) - # -------------------------------------------------------------------- - - name: ๐Ÿ“„ Append Statistics Section - if: always() - run: | - if [ -f "./sections/statistics-section.md" ]; then - echo "๐Ÿ“Š Adding statistics section..." - cat "./sections/statistics-section.md" >> final-report.md - else - echo "โš ๏ธ Statistics section not found, using input content..." - cat << 'EOF' >> final-report.md - ${{ inputs.statistics-report }} - EOF - fi - # Close statistics details, open tests details - { - echo "" - echo "
" - echo "" - echo "
" - echo "๐Ÿงช Test Analysis" - echo "" - } >> final-report.md - - - name: ๐Ÿ“„ Append Tests Section - if: always() - run: | - if [ -f "./sections/tests-section.md" ]; then - echo "๐Ÿงช Adding tests section..." - cat "./sections/tests-section.md" >> final-report.md - else - echo "โš ๏ธ Tests section not found, using input content..." - cat << 'EOF' >> final-report.md - ${{ inputs.tests-report }} - EOF - fi - # Close tests details - echo "" >> final-report.md - echo "
" >> final-report.md - echo "" >> final-report.md - - # -------------------------------------------------------------------- - # Generate Job Results Summary - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Generate Job Results Summary - id: job-results - run: | - # Add fork PR specific information if this is a fork PR (collapsed by default) - 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 That Ran:** Setup, MAGE-X Testing, Code Quality, Pre-Commit$([ "${{ env.INPUT_benchmarks_result }}" != "skipped" ] && echo ", Benchmarks")" - echo "" - echo "**Jobs Skipped (Require Secrets):** Security Scans, Test Suite with Coverage, Release" - echo "" - echo "
" - echo "" - } >> final-report.md - fi - - # Add release-specific information if this was a tag push - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - { - echo "### ๐Ÿ“ฆ Release Information" - } >> final-report.md - - if [[ "${{ env.INPUT_release_result }}" == "success" ]]; then - { - echo "โœ… Release ${{ github.ref_name }} created successfully!" - echo "[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }})" - } >> final-report.md - elif [[ "${{ env.INPUT_release_result }}" == "skipped" ]]; then - echo "โญ๏ธ Release was skipped (likely due to test failures)" >> final-report.md - elif [[ "${{ env.INPUT_release_result }}" == "failure" ]]; then - echo "โŒ Release creation failed - check logs for details" >> final-report.md - fi - echo "" >> final-report.md - fi - - # -------------------------------------------------------------------- - # Generate performance insights (collapsed) - # -------------------------------------------------------------------- - - name: ๐Ÿš€ Generate Performance Insights - id: performance-insights - run: | - TOTAL_DURATION=${TIMING_total_duration:-0} - TOTAL_MINUTES=${TIMING_total_minutes:-0} - TOTAL_SECONDS=${TIMING_total_seconds:-0} - - { - echo "
" - echo "โฑ๏ธ Performance Insights" - echo "" - } >> final-report.md - - # Overall timing insights - if [[ $TOTAL_DURATION -gt 600 ]]; then - echo "- โš ๏ธ Workflow took longer than 10 minutes (${TOTAL_MINUTES}m ${TOTAL_SECONDS}s)" >> final-report.md - elif [[ $TOTAL_DURATION -gt 300 && $TOTAL_DURATION -le 600 ]]; then - echo "- โ„น๏ธ Workflow completed in ${TOTAL_MINUTES}m ${TOTAL_SECONDS}s" >> final-report.md - elif [[ $TOTAL_DURATION -gt 180 && $TOTAL_DURATION -le 300 ]]; then - echo "- ๐ŸŽ‰ Great: Under 5 minutes (${TOTAL_MINUTES}m ${TOTAL_SECONDS}s)" >> final-report.md - elif [[ $TOTAL_DURATION -le 180 ]]; then - echo "- ๐Ÿš€ Excellent: Under 3 minutes!" >> final-report.md - fi - - # Standard insights - { - echo "- **Parallel Jobs**: Multiple jobs ran in parallel" - echo "- **Matrix Strategy**: $(echo '${{ env.INPUT_test_matrix }}' | jq '.include | length') configurations" - } >> final-report.md - - echo "" >> final-report.md - echo "
" >> final-report.md - echo "" >> final-report.md - - # -------------------------------------------------------------------- - # Add compact footer - # -------------------------------------------------------------------- - - name: โœ… Add Report Footer - run: | - { - echo "---" - echo "_๐ŸŽฏ Workflow completed at $(date -u +"%H:%M:%S UTC") โ€” GoFortress CI/CD Pipeline_" - } >> final-report.md - - # -------------------------------------------------------------------- - # Publish final report to GitHub Step Summary - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Publish to GitHub Step Summary - run: | - # Write the final report to GitHub Step Summary - cat final-report.md >> $GITHUB_STEP_SUMMARY - echo "โœ… Completion report generated and published successfully" - - # -------------------------------------------------------------------- - # Upload final report artifact - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload Final Report - id: upload-final - uses: ./.github/actions/upload-statistics - with: - artifact-name: "final-completion-report" - artifact-path: "final-report.md" - retention-days: "7" - - - name: ๐Ÿ“‹ Set Output Content - id: set-output - run: | - echo "content<> $GITHUB_OUTPUT - cat final-report.md >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/fortress-completion-report.yml b/.github/workflows/fortress-completion-report.yml index ac2ae9d..64949dc 100644 --- a/.github/workflows/fortress-completion-report.yml +++ b/.github/workflows/fortress-completion-report.yml @@ -4,9 +4,6 @@ # Purpose: Generate a comprehensive workflow completion report for the entire # workflow run, including timing metrics, test results, job status, and analytics. # -# This workflow orchestrates sub-workflows that process different aspects of the -# completion report to improve maintainability and enable parallel processing. -# # Maintainer: @mrz1836 # # ------------------------------------------------------------------------------------ @@ -36,11 +33,6 @@ on: required: false type: string default: "skipped" - test-magex-result: - description: "Test MAGE-X job result" - required: false - type: string - default: "skipped" security-result: description: "Security job result" required: false @@ -110,26 +102,48 @@ permissions: {} jobs: # ---------------------------------------------------------------------------------- - # Initialize Report and Download Artifacts + # Completion report (single consolidated job) + # + # Workflow phases (each preserved as a step): + # ๐Ÿ”ง Parse environment variables โ†’ expand env-json into $GITHUB_ENV + # โฑ๏ธ Calculate timing metrics โ†’ derive total duration from start-epoch + # ๐Ÿท๏ธ Detect release build โ†’ flag tag pushes for downstream sections + # ๐Ÿ“ฅ Download artifacts โ†’ bench/cache/coverage/fuzz/CI results + # ๐Ÿ—‚๏ธ Flatten artifacts โ†’ move JSON/JSONL into workspace root + # ๐Ÿ”ง Setup MAGE-X โ†’ required for LOC metrics step + # ๐Ÿ“ Initialize section files โ†’ empty statistics-section.md + tests-section.md + # ๐Ÿ’พ๐Ÿ“Šโšก Statistics steps โ†’ cache / benchmark / coverage / LOC + # ๐Ÿงช๐ŸŽ›๏ธ๐ŸŽฏ Test analysis steps โ†’ results / config / fuzz + # ๐Ÿ“ค Upload section artifacts โ†’ preserve for any external consumers (LOC stats + # consumed by go-broadcast analytics) + # ๐Ÿ“ Initialize final report โ†’ status banner + job result matrix + # ๐Ÿ“„ Append statistics + tests โ†’ directly from in-workspace files (no upload/ + # download round-trip needed) + # ๐Ÿ”ง Job results summary โ†’ fork PR + release-specific notes + # ๐Ÿš€ Performance insights โ†’ duration-based annotations + # โœ… Add footer โ†’ timestamp + # ๐Ÿ“‹ Publish to step summary โ†’ final-report.md โ†’ $GITHUB_STEP_SUMMARY + # ๐Ÿ“ค Upload final report โ†’ artifact preserved # ---------------------------------------------------------------------------------- - initialize-report: - name: ๐Ÿ“Š Initialize Report Data + completion-report: + name: ๐Ÿ“Š Generate Completion Report runs-on: ${{ inputs.primary-runner }} + timeout-minutes: 5 if: always() permissions: contents: read actions: read - outputs: - timing-data: ${{ steps.calculate-timing.outputs.timing-json }} steps: # -------------------------------------------------------------------- - # Checkout repository for local actions + # Checkout repository for local actions and helper scripts # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # -------------------------------------------------------------------- - # Parse environment variables + # Parse environment variables from env-json input # -------------------------------------------------------------------- - name: ๐Ÿ”ง Parse environment variables env: @@ -141,7 +155,7 @@ jobs: done # -------------------------------------------------------------------- - # Calculate timing metrics + # Calculate timing metrics (replaces initialize-report job) # -------------------------------------------------------------------- - name: โฑ๏ธ Calculate Timing Metrics id: calculate-timing @@ -153,57 +167,1162 @@ jobs: TOTAL_MINUTES=$((TOTAL_DURATION / 60)) TOTAL_SECONDS=$((TOTAL_DURATION % 60)) - # Store as outputs for later use - echo "total_minutes=$TOTAL_MINUTES" >> $GITHUB_OUTPUT - echo "total_seconds=$TOTAL_SECONDS" >> $GITHUB_OUTPUT - echo "total_duration=$TOTAL_DURATION" >> $GITHUB_OUTPUT + # Store as step outputs for legacy consumers + { + echo "total_minutes=$TOTAL_MINUTES" + echo "total_seconds=$TOTAL_SECONDS" + echo "total_duration=$TOTAL_DURATION" + } >> $GITHUB_OUTPUT - # Create JSON for sub-workflows - echo "timing-json={\"total_minutes\":$TOTAL_MINUTES,\"total_seconds\":$TOTAL_SECONDS,\"total_duration\":$TOTAL_DURATION}" >> $GITHUB_OUTPUT + # Also expose as env vars so later steps can reference TIMING_total_minutes etc. + { + echo "TIMING_total_minutes=$TOTAL_MINUTES" + echo "TIMING_total_seconds=$TOTAL_SECONDS" + echo "TIMING_total_duration=$TOTAL_DURATION" + } >> $GITHUB_ENV - # ---------------------------------------------------------------------------------- - # Process Statistics (Cache, Benchmarks, Coverage, LOC) - # ---------------------------------------------------------------------------------- - process-statistics: - name: ๐Ÿ“Š Process Statistics - needs: initialize-report - if: always() - permissions: - contents: read - actions: read - uses: ./.github/workflows/fortress-completion-statistics.yml - with: - timing-metrics: ${{ needs.initialize-report.outputs.timing-data }} - env-json: ${{ inputs.env-json }} + # -------------------------------------------------------------------- + # Detect if this is a release build (tag) + # -------------------------------------------------------------------- + - name: ๐Ÿท๏ธ Detect Release Build + run: | + # Detect if this is a release build + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + TAG_NAME="${GITHUB_REF#refs/tags/}" + echo "๐Ÿ“ฆ Detected release build (tag: $TAG_NAME)" + echo "IS_RELEASE_BUILD=true" >> $GITHUB_ENV + echo "RELEASE_TAG=$TAG_NAME" >> $GITHUB_ENV + else + echo "๐Ÿ”„ Regular build (non-release)" + echo "IS_RELEASE_BUILD=false" >> $GITHUB_ENV + fi - # ---------------------------------------------------------------------------------- - # Process Test Analysis (Test Results, Fuzz Testing, Configuration) - # ---------------------------------------------------------------------------------- - process-tests: - name: ๐Ÿงช Process Test Analysis - needs: initialize-report - if: always() - permissions: - contents: read - actions: read - uses: ./.github/workflows/fortress-completion-tests.yml - with: - test-suite-result: ${{ inputs.test-suite-result }} - env-json: ${{ inputs.env-json }} + # -------------------------------------------------------------------- + # Download all artifacts needed by both statistics and tests sections + # -------------------------------------------------------------------- + - name: ๐Ÿ“ฅ Download benchmark statistics + if: always() + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "bench-stats-*" + path: ./artifacts/ + merge-multiple: true + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - # ---------------------------------------------------------------------------------- - # Finalize Report (Job Summary, Performance Insights, Assembly) - # ---------------------------------------------------------------------------------- - finalize-report: - name: โœ… Finalize Report - needs: [initialize-report, process-statistics, process-tests] - if: always() - permissions: - contents: read - actions: read - uses: ./.github/workflows/fortress-completion-finalize.yml - with: - all-inputs: ${{ toJSON(inputs) }} - statistics-report: ${{ needs.process-statistics.outputs.report-section }} - tests-report: ${{ needs.process-tests.outputs.report-section }} - timing-data: ${{ needs.initialize-report.outputs.timing-data }} + - name: ๐Ÿ“ฅ Download cache statistics + if: always() + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "cache-stats-*" + path: ./artifacts/ + merge-multiple: true + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} + + - name: ๐Ÿ“ฅ Download internal coverage statistics + if: always() && env.ENABLE_GO_TESTS == 'true' && env.GO_COVERAGE_PROVIDER == 'internal' + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "coverage-stats-internal" + path: ./artifacts/ + merge-multiple: false + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: true + + - name: ๐Ÿ“ฅ Download codecov coverage statistics + if: always() && env.ENABLE_GO_TESTS == 'true' && env.GO_COVERAGE_PROVIDER == 'codecov' + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "coverage-stats-codecov" + path: ./artifacts/ + merge-multiple: false + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: true + + - name: ๐Ÿ“ฅ Download fuzz test failure artifacts + if: always() && env.ENABLE_GO_TESTS == 'true' && env.ENABLE_FUZZ_TESTING == 'true' + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "test-results-fuzz-*" + path: ./test-artifacts/ + merge-multiple: true + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} + + - name: ๐Ÿ“ฅ Download CI results (native mode) + if: always() && env.ENABLE_GO_TESTS == 'true' + uses: ./.github/actions/download-artifact-resilient + with: + pattern: "ci-results-*" + path: ./ci-artifacts/ + merge-multiple: true + max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} + retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} + timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} + continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} + + # -------------------------------------------------------------------- + # Flatten downloaded artifacts into workspace root + # -------------------------------------------------------------------- + - name: ๐Ÿ—‚๏ธ Flatten artifacts + if: always() + run: | + echo "๐Ÿ—‚๏ธ Flattening downloaded artifacts..." + + # Source shared helper functions for artifact processing (used for CI JSONL) + source .github/scripts/parse-test-label.sh || { echo "โŒ Failed to source parse-test-label.sh"; exit 1; } + + if ! type copy_ci_artifact &>/dev/null; then + echo "โŒ Error: copy_ci_artifact function not found after sourcing" + exit 1 + fi + + # Process stats artifacts (bench-stats, cache-stats, coverage-stats JSON files) + if [ -d "./artifacts/" ]; then + find ./artifacts/ -name "*.json" -type f | while read -r file; do + filename=$(basename "$file") + echo "Moving $file to ./$filename" + cp "$file" "./$filename" + done + echo "๐Ÿ“‹ Available stats files:" + ls -la *-stats-*.json 2>/dev/null || echo "No stats files found" + else + echo "โ„น๏ธ No artifacts directory found" + fi + + # Process CI results from ci-artifacts (unit tests) + if [ -d "./ci-artifacts/" ]; then + echo "๐Ÿ“‹ Processing unit test CI results..." + while IFS= read -r -d '' file; do + copy_ci_artifact "$file" "ci" || true + done < <(find ./ci-artifacts/ -name "*.jsonl" -type f -print0 2>/dev/null) + fi + + # Process CI results from test-artifacts (fuzz tests) + if [ -d "./test-artifacts/" ]; then + echo "๐Ÿ“‹ Processing fuzz test CI results..." + while IFS= read -r -d '' file; do + copy_ci_artifact "$file" "ci" || true + done < <(find ./test-artifacts/ -name "*.jsonl" -type f -print0 2>/dev/null) + fi + + echo "๐Ÿ“‹ Available CI results JSONL files:" + ls -la ci-*.jsonl 2>/dev/null || echo "No CI results JSONL files found" + + # -------------------------------------------------------------------- + # Setup MAGE-X for LOC metrics (needed by the LOC step below) + # -------------------------------------------------------------------- + - name: ๐Ÿ”ง Setup MAGE-X for LOC metrics + uses: ./.github/actions/setup-magex + with: + magex-version: ${{ env.MAGE_X_VERSION }} + runner-os: ${{ runner.os }} + use-local: ${{ env.MAGE_X_USE_LOCAL }} + + # -------------------------------------------------------------------- + # Initialize section files (one for each report block) + # -------------------------------------------------------------------- + - name: ๐Ÿ“ Initialize Section Files + run: | + touch statistics-section.md + touch tests-section.md + + # ==================================================================== + # STATISTICS SECTION STEPS + # ==================================================================== + + # -------------------------------------------------------------------- + # Process cache statistics + # -------------------------------------------------------------------- + - name: ๐Ÿ’พ Process Cache Statistics + id: process-cache + run: | + # Process cache statistics if available + if compgen -G "cache-stats-*.json" >/dev/null 2>&1; then + { + echo "" + echo "### ๐Ÿ’พ Cache Statistics" + echo "| Workflow/Job | OS | Go Version | Module Cache | Build Cache | Module Size | Build Size | Redis Cache | Redis Size |" + echo "|--------------|----|-----------|--------------|-----------|-----------|------------|-------------|------------|" + } >> statistics-section.md + + TOTAL_CACHE_HITS=0 + TOTAL_CACHE_ATTEMPTS=0 + WORKFLOWS_WITH_CACHE="" + + for stats_file in cache-stats-*.json; do + if [ -f "$stats_file" ]; then + OS=$(jq -r '.os' "$stats_file") + GO_VER=$(jq -r '.go_version' "$stats_file") + WORKFLOW=$(jq -r '.workflow // "unknown"' "$stats_file") + JOB_NAME=$(jq -r '.job_name // ""' "$stats_file") + GOMOD_HIT=$(jq -r '.gomod_cache_hit' "$stats_file") + GOBUILD_HIT=$(jq -r '.gobuild_cache_hit' "$stats_file") + GOMOD_SIZE=$(jq -r '.cache_size_gomod' "$stats_file") + GOBUILD_SIZE=$(jq -r '.cache_size_gobuild' "$stats_file") + + # Redis cache statistics + REDIS_ENABLED=$(jq -r '.redis_enabled // "false"' "$stats_file") + REDIS_HIT=$(jq -r '.redis_cache_hit // "false"' "$stats_file") + REDIS_SIZE=$(jq -r '.redis_image_size_mb // "0"' "$stats_file") + + GOMOD_ICON=$([[ "$GOMOD_HIT" == "true" ]] && echo "โœ… Hit" || echo "โŒ Miss") + GOBUILD_ICON=$([[ "$GOBUILD_HIT" == "true" ]] && echo "โœ… Hit" || echo "โŒ Miss") + + if [[ "$REDIS_ENABLED" == "true" ]]; then + REDIS_ICON=$([[ "$REDIS_HIT" == "true" ]] && echo "โœ… Hit" || echo "โŒ Miss") + REDIS_SIZE_DISPLAY="${REDIS_SIZE}MB" + else + REDIS_ICON="โž– N/A" + REDIS_SIZE_DISPLAY="โž–" + fi + + if [[ -n "$JOB_NAME" && "$JOB_NAME" != "null" ]]; then + WORKFLOW_JOB="${WORKFLOW}/${JOB_NAME}" + else + WORKFLOW_JOB="${WORKFLOW}" + fi + + echo "| $WORKFLOW_JOB | $OS | $GO_VER | $GOMOD_ICON | $GOBUILD_ICON | $GOMOD_SIZE | $GOBUILD_SIZE | $REDIS_ICON | $REDIS_SIZE_DISPLAY |" >> statistics-section.md + + [[ "$GOMOD_HIT" == "true" ]] && TOTAL_CACHE_HITS=$((TOTAL_CACHE_HITS + 1)) + [[ "$GOBUILD_HIT" == "true" ]] && TOTAL_CACHE_HITS=$((TOTAL_CACHE_HITS + 1)) + [[ "$REDIS_ENABLED" == "true" && "$REDIS_HIT" == "true" ]] && TOTAL_CACHE_HITS=$((TOTAL_CACHE_HITS + 1)) + + TOTAL_CACHE_ATTEMPTS=$((TOTAL_CACHE_ATTEMPTS + 2)) + [[ "$REDIS_ENABLED" == "true" ]] && TOTAL_CACHE_ATTEMPTS=$((TOTAL_CACHE_ATTEMPTS + 1)) + + if [[ "$WORKFLOWS_WITH_CACHE" != *"$WORKFLOW"* ]]; then + if [[ -z "$WORKFLOWS_WITH_CACHE" ]]; then + WORKFLOWS_WITH_CACHE="$WORKFLOW" + else + WORKFLOWS_WITH_CACHE="${WORKFLOWS_WITH_CACHE}, $WORKFLOW" + fi + fi + fi + done + + if [[ $TOTAL_CACHE_ATTEMPTS -gt 0 ]]; then + CACHE_HIT_RATE=$((TOTAL_CACHE_HITS * 100 / TOTAL_CACHE_ATTEMPTS)) + { + echo "" + echo "**Cache Performance Summary:**" + echo "- **Overall Hit Rate**: ${CACHE_HIT_RATE}% (${TOTAL_CACHE_HITS}/${TOTAL_CACHE_ATTEMPTS} cache operations)" + echo "- **Workflows Using Cache**: $WORKFLOWS_WITH_CACHE" + } >> statistics-section.md + + if [[ $CACHE_HIT_RATE -ge 80 ]]; then + echo "- **Cache Efficiency**: ๐Ÿš€ Excellent (${CACHE_HIT_RATE}% hit rate)" >> statistics-section.md + elif [[ $CACHE_HIT_RATE -ge 60 ]]; then + echo "- **Cache Efficiency**: โœ… Good (${CACHE_HIT_RATE}% hit rate)" >> statistics-section.md + elif [[ $CACHE_HIT_RATE -ge 40 ]]; then + echo "- **Cache Efficiency**: โš ๏ธ Fair (${CACHE_HIT_RATE}% hit rate)" >> statistics-section.md + else + echo "- **Cache Efficiency**: โŒ Poor (${CACHE_HIT_RATE}% hit rate - consider optimizing cache strategy)" >> statistics-section.md + fi + + echo "cache-metrics={\"hit_rate\":$CACHE_HIT_RATE,\"total_hits\":$TOTAL_CACHE_HITS,\"total_attempts\":$TOTAL_CACHE_ATTEMPTS}" >> $GITHUB_OUTPUT + fi + + echo "" >> statistics-section.md + echo "

" >> statistics-section.md + else + { + 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 + + # -------------------------------------------------------------------- + # Process benchmark statistics + # -------------------------------------------------------------------- + - name: โšก Process Benchmark Statistics + id: process-benchmarks + run: | + if compgen -G "bench-stats-*.json" >/dev/null 2>&1; then + { + echo "" + echo "" + echo "### โšก Benchmark Results" + } >> statistics-section.md + + # Get benchmark mode from the first stats file + BENCH_MODE="normal" + for stats_file in bench-stats-*.json; do + if [ -f "$stats_file" ]; then + BENCH_MODE=$(jq -r '.benchmark_mode // "normal"' "$stats_file") + break + fi + done + + { + echo "" + echo "**Mode**: \`$BENCH_MODE\` $(case "$BENCH_MODE" in quick) echo "(Quick 50ms runs)" ;; full) echo "(Comprehensive 10s runs)" ;; *) echo "(Normal 100ms runs)" ;; esac)" + echo "" + echo "| Benchmark Suite | Duration | Benchmarks | Status |" + echo "|-----------------|----------|------------|--------|" + } >> statistics-section.md + + TOTAL_BENCHMARKS=0 + TOTAL_DURATION=0 + + for stats_file in bench-stats-*.json; do + if [ -f "$stats_file" ]; then + NAME=$(jq -r '.name' "$stats_file") + DURATION=$(jq -r '.duration_seconds' "$stats_file") + BENCHMARK_COUNT=$(jq -r '.benchmark_count' "$stats_file") + STATUS=$(jq -r '.status' "$stats_file") + + DURATION_MIN=$((DURATION / 60)) + DURATION_SEC=$((DURATION % 60)) + STATUS_ICON=$([[ "$STATUS" == "success" ]] && echo "โœ…" || echo "โŒ") + + echo "| $NAME | ${DURATION_MIN}m ${DURATION_SEC}s | $BENCHMARK_COUNT | $STATUS_ICON |" >> statistics-section.md + + TOTAL_BENCHMARKS=$((TOTAL_BENCHMARKS + BENCHMARK_COUNT)) + TOTAL_DURATION=$((TOTAL_DURATION + DURATION)) + fi + done + + { + echo "" + echo "
" + echo "Detailed Benchmark Results" + echo "" + } >> statistics-section.md + + for stats_file in bench-stats-*.json; do + if [ -f "$stats_file" ]; then + NAME=$(jq -r '.name' "$stats_file") + BENCHMARK_SUMMARY=$(jq -r '.benchmark_summary' "$stats_file") + if [ -n "$BENCHMARK_SUMMARY" ] && [ "$BENCHMARK_SUMMARY" != "null" ]; then + { + echo "#### $NAME" + echo "$BENCHMARK_SUMMARY" + echo "" + } >> statistics-section.md + fi + fi + done + + echo "


" >> statistics-section.md + + echo "benchmark-metrics={\"total_benchmarks\":$TOTAL_BENCHMARKS,\"total_duration\":$TOTAL_DURATION,\"mode\":\"$BENCH_MODE\"}" >> $GITHUB_OUTPUT + else + { + 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 + + # -------------------------------------------------------------------- + # Process coverage statistics + # -------------------------------------------------------------------- + - name: ๐Ÿ“ˆ Process Coverage Statistics + id: process-coverage + run: | + echo "๐Ÿ” Looking for coverage statistics files..." + + COVERAGE_FILES_FOUND=false + if compgen -G "coverage-stats-*.json" >/dev/null 2>&1; then + echo "๐Ÿ“‹ Found coverage-stats-*.json files:" + ls -la coverage-stats-*.json || echo "None" + COVERAGE_FILES_FOUND=true + fi + + if compgen -G "coverage-stats-internal-*.json" >/dev/null 2>&1; then + echo "๐Ÿ“‹ Found coverage-stats-internal-*.json files:" + ls -la coverage-stats-internal-*.json || echo "None" + COVERAGE_FILES_FOUND=true + fi + + if [[ "$COVERAGE_FILES_FOUND" == "true" ]]; then + HAS_COVERAGE_DATA=false + VALID_COVERAGE_FILE="" + UPDATED_FILE_FOUND=false + + for pattern in "coverage-stats-*.json" "coverage-stats-internal-*.json"; do + if compgen -G "$pattern" >/dev/null 2>&1; then + # First, prioritize any "updated" statistics files + for stats_file in $pattern; do + if [ -f "$stats_file" ] && [[ "$stats_file" == *"updated"* ]]; then + echo "๐Ÿ” Checking UPDATED statistics file: $stats_file" + COVERAGE_PERCENT=$(jq -r '.coverage_percent // .coverage_percentage // "null"' "$stats_file") + echo " - Coverage value found: '$COVERAGE_PERCENT'" + + if [[ "$COVERAGE_PERCENT" != "null" ]] && [[ "$COVERAGE_PERCENT" != "N/A" ]] && [[ -n "$COVERAGE_PERCENT" ]]; then + echo "โœ… Valid coverage data found in UPDATED file: $stats_file" + HAS_COVERAGE_DATA=true + VALID_COVERAGE_FILE="$stats_file" + UPDATED_FILE_FOUND=true + break 2 + fi + fi + done + + if [[ "$UPDATED_FILE_FOUND" == "false" ]]; then + for stats_file in $pattern; do + if [ -f "$stats_file" ] && [[ "$stats_file" != *"updated"* ]]; then + echo "๐Ÿ” Checking $stats_file for valid coverage data..." + COVERAGE_PERCENT=$(jq -r '.coverage_percent // .coverage_percentage // "null"' "$stats_file") + echo " - Coverage value found: '$COVERAGE_PERCENT'" + + if [[ "$COVERAGE_PERCENT" != "null" ]] && [[ "$COVERAGE_PERCENT" != "N/A" ]] && [[ -n "$COVERAGE_PERCENT" ]]; then + echo "โœ… Valid coverage data found in $stats_file" + HAS_COVERAGE_DATA=true + VALID_COVERAGE_FILE="$stats_file" + break 2 + else + echo "โš ๏ธ No valid coverage data in $stats_file" + fi + fi + done + fi + fi + done + + if [[ "$HAS_COVERAGE_DATA" == "true" ]] && [[ -n "$VALID_COVERAGE_FILE" ]]; then + { + echo "" + echo "

" + echo "" + echo "### ๐Ÿ“ˆ Code Coverage Report" + } >> statistics-section.md + + echo "๐Ÿ“Š Processing coverage data from: $VALID_COVERAGE_FILE" + + COVERAGE_PERCENT=$(jq -r '.coverage_percent // .coverage_percentage // "N/A"' "$VALID_COVERAGE_FILE") + PROCESSING_TIME=$(jq -r '.processing_time_seconds // "N/A"' "$VALID_COVERAGE_FILE") + FILES_PROCESSED=$(jq -r '.files_processed // "N/A"' "$VALID_COVERAGE_FILE") + BADGE_GENERATED=$(jq -r '.badge_generated // "false"' "$VALID_COVERAGE_FILE") + PAGES_DEPLOYED=$(jq -r '.pages_deployed // "false"' "$VALID_COVERAGE_FILE") + COVERAGE_PROVIDER=$(jq -r '.provider // "N/A"' "$VALID_COVERAGE_FILE") + + echo "๐Ÿ“‹ Coverage metrics: ${COVERAGE_PERCENT}%, ${FILES_PROCESSED} files, ${PROCESSING_TIME}s processing" + + { + echo "| Metric | Value |" + echo "|--------|-------|" + echo "| **Coverage Percentage** | $COVERAGE_PERCENT% |" + echo "| **Processing Time** | ${PROCESSING_TIME}s |" + echo "| **Files Processed** | $FILES_PROCESSED |" + echo "| **Coverage Provider** | $([ "$COVERAGE_PROVIDER" == "internal" ] && echo "go-coverage" || ([ "$COVERAGE_PROVIDER" == "codecov" ] && echo "Codecov" || echo "$COVERAGE_PROVIDER")) |" + echo "| **Badge Generated** | $([ "$BADGE_GENERATED" == "true" ] && echo "โœ… Yes" || echo "โŒ No") |" + echo "| **Pages Deployed** | $([ "$PAGES_DEPLOYED" == "true" ] && echo "โœ… Yes" || echo "โŒ No") |" + } >> statistics-section.md + + echo "coverage-metrics={\"percentage\":\"$COVERAGE_PERCENT\",\"files_processed\":\"$FILES_PROCESSED\",\"processing_time\":\"$PROCESSING_TIME\",\"provider\":\"$COVERAGE_PROVIDER\"}" >> $GITHUB_OUTPUT + fi + elif [[ "${{ env.ENABLE_CODE_COVERAGE }}" == "true" ]]; then + { + echo "" + echo "

" + echo "" + echo "### ๐Ÿ“ˆ Code Coverage Status" + echo "| Status | Details |" + echo "|--------|---------|" + + if [[ "${IS_RELEASE_BUILD:-false}" == "true" ]]; then + echo "| **Coverage** | ๐Ÿ“ฆ Coverage analysis skipped for release builds |" + echo "| **Reason** | Coverage processing is disabled during releases to optimize build time |" + if [[ -n "${RELEASE_TAG:-}" ]]; then + echo "| **Release Tag** | \`${RELEASE_TAG}\` |" + fi + else + echo "| **Coverage** | โš ๏ธ No coverage data available - check job logs |" + fi + + echo "| **Threshold** | ${{ env.GO_COVERAGE_THRESHOLD }}% minimum |" + echo "| **Badge Style** | ${{ env.GO_COVERAGE_BADGE_STYLE }} |" + echo "| **PR Comments** | $([ "${{ env.GO_COVERAGE_POST_COMMENTS }}" == "true" ] && echo "โœ… Enabled" || echo "โŒ Disabled") |" + echo "| **Theme** | ${{ env.GO_COVERAGE_REPORT_THEME }} |" + } >> statistics-section.md + fi + + # -------------------------------------------------------------------- + # Generate Lines of Code Summary + # -------------------------------------------------------------------- + - name: ๐Ÿ“Š Generate Lines of Code Summary + id: process-loc + run: | + echo "๐Ÿ“Š Running magex metrics:loc json..." + + LOC_OUTPUT=$(magex metrics:loc json 2>&1 || true) + LOC_FOUND=false + + # Save raw JSON for loc-stats artifact (consumed by go-broadcast analytics) + if [[ -n "$LOC_OUTPUT" ]] && echo "$LOC_OUTPUT" | jq empty 2>/dev/null; then + echo "$LOC_OUTPUT" > loc-stats.json + echo "๐Ÿ“ฆ Saved loc-stats.json for artifact upload" + fi + + if [[ -n "$LOC_OUTPUT" ]]; then + echo "๐Ÿ“‹ magex metrics:loc json output:" + echo "$LOC_OUTPUT" + + TEST_FILES_LOC=$(echo "$LOC_OUTPUT" | jq -r '.test_files_loc // empty') + TEST_FILES_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.test_files_count // empty') + GO_FILES_LOC=$(echo "$LOC_OUTPUT" | jq -r '.go_files_loc // empty') + GO_FILES_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.go_files_count // empty') + TOTAL_LOC=$(echo "$LOC_OUTPUT" | jq -r '.total_loc // empty') + TOTAL_FILES_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.total_files_count // empty') + LOC_DATE=$(echo "$LOC_OUTPUT" | jq -r '.date // empty') + + TEST_FILES_SIZE=$(echo "$LOC_OUTPUT" | jq -r '.test_files_size_human // empty') + GO_FILES_SIZE=$(echo "$LOC_OUTPUT" | jq -r '.go_files_size_human // empty') + TOTAL_SIZE=$(echo "$LOC_OUTPUT" | jq -r '.total_size_human // empty') + TEST_AVG_SIZE_BYTES=$(echo "$LOC_OUTPUT" | jq -r '.test_avg_size_bytes // empty') + GO_AVG_SIZE_BYTES=$(echo "$LOC_OUTPUT" | jq -r '.go_avg_size_bytes // empty') + + AVG_LINES_PER_FILE=$(echo "$LOC_OUTPUT" | jq -r '.avg_lines_per_file // empty') + TEST_COVERAGE_RATIO=$(echo "$LOC_OUTPUT" | jq -r '.test_coverage_ratio // empty') + PACKAGE_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.package_count // empty') + + echo " - Test Files LOC: '$TEST_FILES_LOC' (count: $TEST_FILES_COUNT)" + echo " - Go Files LOC: '$GO_FILES_LOC' (count: $GO_FILES_COUNT)" + echo " - Total LOC: '$TOTAL_LOC' (files: $TOTAL_FILES_COUNT)" + echo " - Date: '$LOC_DATE'" + + if [[ -n "$TEST_FILES_LOC" ]] && [[ -n "$GO_FILES_LOC" ]] && [[ -n "$TOTAL_LOC" ]]; then + LOC_FOUND=true + echo "โœ… Successfully parsed LOC JSON data" + + if [[ -z "$TOTAL_SIZE" ]] || [[ -z "$AVG_LINES_PER_FILE" ]]; then + echo "โš ๏ธ Some enhanced metrics are missing (older magex version?)" + fi + fi + else + echo "โš ๏ธ No output from magex metrics:loc json" + fi + + if [[ "$LOC_FOUND" == "true" ]]; then + DISPLAY_TEST_LOC=$(LC_NUMERIC=en_US.UTF-8 printf "%'d" "${TEST_FILES_LOC:-0}") + DISPLAY_TEST_COUNT="${TEST_FILES_COUNT:-N/A}" + DISPLAY_GO_LOC=$(LC_NUMERIC=en_US.UTF-8 printf "%'d" "${GO_FILES_LOC:-0}") + DISPLAY_GO_COUNT="${GO_FILES_COUNT:-N/A}" + DISPLAY_TOTAL_LOC=$(LC_NUMERIC=en_US.UTF-8 printf "%'d" "${TOTAL_LOC:-0}") + DISPLAY_TOTAL_FILES="${TOTAL_FILES_COUNT:-N/A}" + DISPLAY_LOC_DATE="${LOC_DATE:-N/A}" + + if [[ -n "$TEST_AVG_SIZE_BYTES" ]] && [[ "$TEST_AVG_SIZE_BYTES" != "0" ]]; then + DISPLAY_TEST_AVG_SIZE=$(numfmt --to=iec-i --suffix=B "$TEST_AVG_SIZE_BYTES" 2>/dev/null || echo "${TEST_AVG_SIZE_BYTES}B") + else + DISPLAY_TEST_AVG_SIZE="N/A" + fi + + if [[ -n "$GO_AVG_SIZE_BYTES" ]] && [[ "$GO_AVG_SIZE_BYTES" != "0" ]]; then + DISPLAY_GO_AVG_SIZE=$(numfmt --to=iec-i --suffix=B "$GO_AVG_SIZE_BYTES" 2>/dev/null || echo "${GO_AVG_SIZE_BYTES}B") + else + DISPLAY_GO_AVG_SIZE="N/A" + fi + + DISPLAY_TEST_SIZE="${TEST_FILES_SIZE:-N/A}" + DISPLAY_GO_SIZE="${GO_FILES_SIZE:-N/A}" + DISPLAY_TOTAL_SIZE="${TOTAL_SIZE:-N/A}" + + { + echo "" + echo "

" + echo "" + echo "### ๐Ÿ“Š Lines of Code Summary" + echo "| Type | Lines of Code | Files | Total Size | Avg Size | Date |" + echo "|------|---------------|-------|------------|----------|------|" + echo "| Test Files | $DISPLAY_TEST_LOC | $DISPLAY_TEST_COUNT | $DISPLAY_TEST_SIZE | $DISPLAY_TEST_AVG_SIZE | $DISPLAY_LOC_DATE |" + echo "| Go Files | $DISPLAY_GO_LOC | $DISPLAY_GO_COUNT | $DISPLAY_GO_SIZE | $DISPLAY_GO_AVG_SIZE | $DISPLAY_LOC_DATE |" + echo "| **Total** | **$DISPLAY_TOTAL_LOC** | **$DISPLAY_TOTAL_FILES** | **$DISPLAY_TOTAL_SIZE** | | |" + echo "" + + if [[ -n "$AVG_LINES_PER_FILE" ]] || [[ -n "$TEST_COVERAGE_RATIO" ]] || [[ -n "$PACKAGE_COUNT" ]]; then + echo "#### ๐Ÿ“ˆ Code Quality Metrics" + echo "" + echo "| Metric | Value |" + echo "|--------|-------|" + + if [[ -n "$AVG_LINES_PER_FILE" ]]; then + DISPLAY_AVG_LINES=$(LC_NUMERIC=en_US.UTF-8 printf "%.1f" "${AVG_LINES_PER_FILE}") + echo "| Average Lines per File | $DISPLAY_AVG_LINES |" + fi + + if [[ -n "$TEST_COVERAGE_RATIO" ]]; then + DISPLAY_COVERAGE=$(LC_NUMERIC=en_US.UTF-8 printf "%.1f%%" "${TEST_COVERAGE_RATIO}") + echo "| Test Coverage Ratio | $DISPLAY_COVERAGE |" + fi + + if [[ -n "$PACKAGE_COUNT" ]]; then + echo "| Package/Directory Count | $PACKAGE_COUNT |" + fi + + if [[ -n "$TOTAL_SIZE" ]]; then + echo "| Total Project Size | $TOTAL_SIZE |" + fi + + echo "" + fi + + echo "

" + } >> statistics-section.md + + echo "โœ… LOC section added: Test=$DISPLAY_TEST_LOC ($DISPLAY_TEST_COUNT files), Go=$DISPLAY_GO_LOC ($DISPLAY_GO_COUNT files), Total=$DISPLAY_TOTAL_LOC ($DISPLAY_TOTAL_FILES files)" + else + echo "โš ๏ธ Could not collect LOC data" + { + echo "" + echo "

" + echo "" + echo "### ๐Ÿ“Š Lines of Code Summary" + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Lines of Code** | โŒ Data not available |" + echo "| **Reason** | magex metrics:loc json command failed or produced unexpected output |" + echo "" + echo "

" + } >> statistics-section.md + fi + + # ==================================================================== + # TEST ANALYSIS STEPS + # ==================================================================== + + # -------------------------------------------------------------------- + # Process test statistics + # -------------------------------------------------------------------- + - name: ๐Ÿงช Process Test Statistics + id: process-tests + run: | + source .github/scripts/parse-test-label.sh || { echo "โŒ Failed to source parse-test-label.sh"; exit 1; } + + shopt -s nullglob + + TOTAL_TESTS=0 + TOTAL_FAILURES=0 + TOTAL_PASSED=0 + TOTAL_SKIPPED=0 + SUITE_COUNT=0 + HAS_DATA=false + + if compgen -G "ci-*.jsonl" >/dev/null 2>&1; then + echo "๐Ÿ“Š Processing native CI mode JSONL files..." + HAS_DATA=true + + { + echo "" + echo "" + echo "### ๐Ÿงช Test Results Summary" + echo "| Test Suite | Duration | Tests | Runs | Passed | Failed | Skipped | Status |" + echo "|------------|----------|-------|------|--------|--------|---------|--------|" + } >> tests-section.md + + for jsonl_file in ci-*.jsonl; do + if [ -f "$jsonl_file" ]; then + ARTIFACT_NAME=$(echo "$jsonl_file" | sed 's/^ci-//' | sed 's/-ci-results\.jsonl$//') + SUITE_LABEL=$(parse_test_label "$ARTIFACT_NAME") + + SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") + + if [[ -n "$SUMMARY" ]]; then + STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') + PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') + FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') + SKIPPED=$(echo "$SUMMARY" | jq -r '.summary.skipped // 0') + TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') + UNIQUE=$(echo "$SUMMARY" | jq -r '.summary.unique_total // .summary.total // 0') + DURATION=$(echo "$SUMMARY" | jq -r '.summary.duration // "0s"') + + STATUS_ICON=$([[ "$STATUS" == "passed" ]] && echo "โœ…" || echo "โŒ") + + echo "| $SUITE_LABEL | $DURATION | $UNIQUE | $TOTAL | $PASSED | $FAILED | $SKIPPED | $STATUS_ICON |" >> tests-section.md + + TOTAL_TESTS=$((TOTAL_TESTS + UNIQUE)) + TOTAL_PASSED=$((TOTAL_PASSED + PASSED)) + TOTAL_FAILURES=$((TOTAL_FAILURES + FAILED)) + TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED)) + SUITE_COUNT=$((SUITE_COUNT + 1)) + fi + fi + done + + echo "test-metrics={\"total_tests\":$TOTAL_TESTS,\"total_failures\":$TOTAL_FAILURES,\"suite_count\":$SUITE_COUNT}" >> $GITHUB_OUTPUT + + if [[ $TOTAL_FAILURES -gt 0 ]]; then + { + echo "" + echo "" + echo "### โŒ Test Failure Analysis" + echo "**Total Failures**: $TOTAL_FAILURES across $SUITE_COUNT test suite(s)" + echo "" + } >> tests-section.md + + echo "#### ๐Ÿ“Š Failures by Test Suite:" >> tests-section.md + for jsonl_file in ci-*.jsonl; do + if [ -f "$jsonl_file" ]; then + ARTIFACT_NAME=$(echo "$jsonl_file" | sed 's/^ci-//' | sed 's/-ci-results\.jsonl$//') + SUITE_LABEL=$(parse_test_label "$ARTIFACT_NAME") + SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") + + if [[ -n "$SUMMARY" ]]; then + FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') + if [[ $FAILED -gt 0 ]]; then + echo "- **$SUITE_LABEL**: $FAILED failures" >> tests-section.md + fi + fi + fi + done + + { + echo "" + echo "
" + echo "๐Ÿ” Failed Tests (click to expand)" + echo "" + echo "| Test Name | Package | Error |" + echo "|-----------|---------|-------|" + } >> tests-section.md + + FAILURE_COUNT=0 + for jsonl_file in ci-*.jsonl; do + if [ -f "$jsonl_file" ] && [[ $FAILURE_COUNT -lt 20 ]]; then + while read -r line; do + if [[ $FAILURE_COUNT -ge 20 ]]; then + break + fi + + TEST=$(echo "$line" | jq -r '.failure.test // "unknown"') + PKG=$(echo "$line" | jq -r '.failure.package // "unknown"' | sed 's|.*/||') + ERROR=$(echo "$line" | jq -r '.failure.error // ""' | head -c 100 | tr '\n' ' ') + + if [[ ${#ERROR} -gt 80 ]]; then + ERROR="${ERROR:0:77}..." + fi + + echo "| \`$TEST\` | $PKG | ${ERROR:-_no message_} |" + FAILURE_COUNT=$((FAILURE_COUNT + 1)) + done < <(grep '"type":"failure"' "$jsonl_file" 2>/dev/null) >> tests-section.md || true + fi + done + + { + echo "" + echo "
" + } >> tests-section.md + + echo "failure-metrics={\"total_failures\":$TOTAL_FAILURES,\"has_error_output\":true}" >> $GITHUB_OUTPUT + fi + fi + + if [[ "$HAS_DATA" == "false" ]]; then + { + echo "" + echo "" + echo "### ๐Ÿงช Test Results Summary" + echo "" + echo "| Status | Details |" + echo "|--------|---------|" + if [[ "${{ env.ENABLE_GO_TESTS }}" == "false" ]]; then + echo "| **Test Suite** | โŒ Disabled - Set ENABLE_GO_TESTS=true to enable |" + echo "| **Reason** | Tests are disabled via configuration flag |" + echo "| **Note** | Enable ENABLE_GO_TESTS in .github/env/00-core.env to run tests |" + else + 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._" + fi + } >> tests-section.md + fi + + # -------------------------------------------------------------------- + # Add test configuration section + # -------------------------------------------------------------------- + - name: ๐ŸŽ›๏ธ Add Test Configuration Section + id: add-test-config + run: | + HAS_CONFIG_DATA=false + + if compgen -G "ci-*.jsonl" >/dev/null 2>&1; then + HAS_CONFIG_DATA=true + { + echo "" + echo "

" + echo "" + echo "### ๐ŸŽ›๏ธ Test Output Configuration" + echo "" + echo "**Output Mode**: Native CI Mode (JSONL)" + echo "" + echo "- Tests executed with magex native CI mode" + echo "- Structured output in .mage-x/ci-results.jsonl" + echo "- Automatic GitHub annotations for failures" + } >> tests-section.md + fi + + if [[ "$HAS_CONFIG_DATA" == "false" ]]; then + echo "" >> tests-section.md + echo "โ„น๏ธ _Test configuration section skipped - no test data available_" >> tests-section.md + fi + + # -------------------------------------------------------------------- + # Process fuzz test statistics + # -------------------------------------------------------------------- + - name: ๐ŸŽฏ Process Fuzz Test Statistics + id: process-fuzz + run: | + { + echo "

" + echo "" + echo "### ๐Ÿ›ก๏ธ Security Testing Results" + } >> tests-section.md + + if [[ "${{ env.ENABLE_FUZZ_TESTING }}" == "true" ]]; then + FUZZ_JSONL=$(ls ci-*-ci-results-fuzz.jsonl 2>/dev/null | head -1 || echo "") + + if [[ -n "$FUZZ_JSONL" ]] && [[ -f "$FUZZ_JSONL" ]]; then + SUMMARY=$(grep '"type":"summary"' "$FUZZ_JSONL" 2>/dev/null | head -1 || echo "") + + if [[ -n "$SUMMARY" ]]; then + STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') + TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') + DURATION=$(echo "$SUMMARY" | jq -r '.summary.duration // "0s"') + + STATUS_ICON=$([[ "$STATUS" == "passed" ]] && echo "โœ…" || echo "โŒ") + + { + echo "| Fuzz Suite | Duration | Fuzz Tests | Status | Enabled |" + echo "|------------|----------|------------|--------|---------|" + echo "| Fuzz Tests | $DURATION | $TOTAL | $STATUS_ICON | ๐ŸŽฏ |" + } >> tests-section.md + else + { + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Fuzz Testing** | โœ… Enabled |" + echo "| **Execution** | โš ๏ธ No fuzz summary found in JSONL - check job logs |" + echo "| **Platform** | Linux with primary Go version |" + } >> tests-section.md + fi + else + { + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Fuzz Testing** | โœ… Enabled |" + echo "| **Execution** | โš ๏ธ No fuzz results found - check job logs |" + echo "| **Platform** | Linux with primary Go version |" + } >> tests-section.md + fi + else + { + echo "| Status | Details |" + echo "|--------|---------|" + echo "| **Fuzz Testing** | โŒ Disabled |" + echo "| **Configuration** | Set ENABLE_FUZZ_TESTING=true to enable |" + echo "| **Target Platform** | Would run on Linux with primary Go version |" + } >> tests-section.md + fi + + # ==================================================================== + # ARTIFACT UPLOADS (preserve names for any external consumers) + # ==================================================================== + + - name: ๐Ÿ“ค Upload LOC Stats JSON + if: always() && hashFiles('loc-stats.json') != '' + uses: ./.github/actions/upload-artifact-resilient + with: + artifact-name: loc-stats + artifact-path: loc-stats.json + retention-days: "7" + + - name: ๐Ÿ“ค Upload Statistics Artifact + if: always() + uses: ./.github/actions/upload-statistics + with: + artifact-name: "statistics-section" + artifact-path: "statistics-section.md" + retention-days: "7" + if-no-files-found: "warn" + + - name: ๐Ÿ“ค Upload Tests Artifact + if: always() + uses: ./.github/actions/upload-statistics + with: + artifact-name: "tests-section" + artifact-path: "tests-section.md" + retention-days: "7" + if-no-files-found: "warn" + + # ==================================================================== + # FINAL REPORT ASSEMBLY + # ==================================================================== + + # -------------------------------------------------------------------- + # Initialize final report with STATUS BANNER FIRST + # -------------------------------------------------------------------- + - name: ๐Ÿ“ Initialize Final Report + env: + SETUP_RESULT: ${{ inputs.setup-result }} + PRE_COMMIT_RESULT: ${{ inputs.pre-commit-result }} + SECURITY_RESULT: ${{ inputs.security-result }} + CODE_QUALITY_RESULT: ${{ inputs.code-quality-result }} + TEST_SUITE_RESULT: ${{ inputs.test-suite-result }} + BENCHMARKS_RESULT: ${{ inputs.benchmarks-result }} + RELEASE_RESULT: ${{ inputs.release-result }} + run: | + # Determine overall workflow status + WORKFLOW_FAILED=false + FAILED_JOBS="" + + # Check each critical job result + if [[ "$SETUP_RESULT" != "success" && "$SETUP_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Setup Configuration\n" + fi + if [[ "$PRE_COMMIT_RESULT" != "success" && "$PRE_COMMIT_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Pre-commit Checks\n" + fi + if [[ "$SECURITY_RESULT" != "success" && "$SECURITY_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Security Scans\n" + fi + if [[ "$CODE_QUALITY_RESULT" != "success" && "$CODE_QUALITY_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Code Quality\n" + fi + if [[ "$TEST_SUITE_RESULT" != "success" && "$TEST_SUITE_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Test Suite\n" + fi + if [[ "$BENCHMARKS_RESULT" != "success" && "$BENCHMARKS_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Benchmarks\n" + fi + if [[ "$RELEASE_RESULT" != "success" && "$RELEASE_RESULT" != "skipped" ]]; then + WORKFLOW_FAILED=true + FAILED_JOBS="$FAILED_JOBS- โŒ Release\n" + fi + + SUMMARY_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + + { + # ================================================================= + # STATUS BANNER (Always visible at top - immediate failure visibility) + # ================================================================= + echo "# ๐Ÿ Workflow Complete" + echo "" + + if [[ "$WORKFLOW_FAILED" == "true" ]]; then + echo "> [!CAUTION]" + echo "> ## ๐Ÿ”ด WORKFLOW FAILED" + echo ">" + echo "> **Failed Jobs:**" + echo -e "$FAILED_JOBS" | while IFS= read -r line; do echo "> $line"; done + echo "" + else + echo "> [!TIP]" + echo "> ## ๐ŸŸข ALL CHECKS PASSED" + fi + echo "" + echo "| Job | Result |" + echo "|-----|--------|" + echo "| Setup Configuration | $([ "$SETUP_RESULT" = "success" ] && echo "โœ… Passed" || ([ "$SETUP_RESULT" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" + echo "| Pre-commit Checks | $([ "$PRE_COMMIT_RESULT" = "success" ] && echo "โœ… Passed" || ([ "$PRE_COMMIT_RESULT" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" + echo "| Security Scans | $([ "$SECURITY_RESULT" = "success" ] && echo "โœ… Passed" || ([ "$SECURITY_RESULT" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" + echo "| Code Quality | $([ "$CODE_QUALITY_RESULT" = "success" ] && echo "โœ… Passed" || ([ "$CODE_QUALITY_RESULT" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" + echo "| Test Suite | $([ "$TEST_SUITE_RESULT" = "success" ] && echo "โœ… Passed" || ([ "$TEST_SUITE_RESULT" = "skipped" ] && echo "โญ๏ธ Skipped" || echo "โŒ Failed")) |" + # Only show benchmarks if attempted + if [[ "$BENCHMARKS_RESULT" != "skipped" ]]; then + echo "| Benchmarks | $([ "$BENCHMARKS_RESULT" = "success" ] && echo "โœ… Passed" || echo "โŒ Failed") |" + fi + # Only show release if attempted + if [[ "$RELEASE_RESULT" != "skipped" ]]; then + echo "| Release | $([ "$RELEASE_RESULT" = "success" ] && echo "โœ… Passed" || echo "โŒ Failed") |" + fi + echo "" + echo "**Duration:** ${TIMING_total_minutes:-0}m ${TIMING_total_seconds:-0}s" + echo "" + echo "**Generated:** $SUMMARY_TIME" + echo "" + + # ================================================================= + # DETAILED SECTIONS (Collapsed by default) + # ================================================================= + echo "
" + echo "๐Ÿ“Š Statistics (Cache, Coverage, LOC)" + echo "" + } > final-report.md + + # -------------------------------------------------------------------- + # Append statistics + tests sections directly from in-workspace files + # -------------------------------------------------------------------- + - name: ๐Ÿ“„ Append Statistics Section + if: always() + run: | + if [ -f "statistics-section.md" ]; then + echo "๐Ÿ“Š Adding statistics section..." + cat statistics-section.md >> final-report.md + else + echo "โš ๏ธ Statistics section not found (unexpected โ€” file should exist in same job)" + fi + # Close statistics details, open tests details + { + echo "" + echo "
" + echo "" + echo "
" + echo "๐Ÿงช Test Analysis" + echo "" + } >> final-report.md + + - name: ๐Ÿ“„ Append Tests Section + if: always() + run: | + if [ -f "tests-section.md" ]; then + echo "๐Ÿงช Adding tests section..." + cat tests-section.md >> final-report.md + else + echo "โš ๏ธ Tests section not found (unexpected โ€” file should exist in same job)" + fi + # Close tests details + echo "" >> final-report.md + echo "
" >> final-report.md + echo "" >> final-report.md + + # -------------------------------------------------------------------- + # Generate Job Results Summary (fork PR + release info) + # -------------------------------------------------------------------- + - name: ๐Ÿ”ง Generate Job Results Summary + id: job-results + env: + IS_FORK_PR: ${{ inputs.is-fork-pr }} + FORK_SECURITY_MODE: ${{ inputs.fork-security-mode }} + BENCHMARKS_RESULT: ${{ inputs.benchmarks-result }} + RELEASE_RESULT: ${{ inputs.release-result }} + run: | + # Add fork PR specific information if this is a fork PR (collapsed by default) + if [[ "$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:** \`$FORK_SECURITY_MODE\`" + echo "" + echo "**Jobs That Ran:** Setup, MAGE-X Testing, Code Quality, Pre-Commit$([ "$BENCHMARKS_RESULT" != "skipped" ] && echo ", Benchmarks")" + echo "" + echo "**Jobs Skipped (Require Secrets):** Security Scans, Test Suite with Coverage, Release" + echo "" + echo "
" + echo "" + } >> final-report.md + fi + + # Add release-specific information if this was a tag push + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + { + echo "### ๐Ÿ“ฆ Release Information" + } >> final-report.md + + if [[ "$RELEASE_RESULT" == "success" ]]; then + { + echo "โœ… Release ${{ github.ref_name }} created successfully!" + echo "[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }})" + } >> final-report.md + elif [[ "$RELEASE_RESULT" == "skipped" ]]; then + echo "โญ๏ธ Release was skipped (likely due to test failures)" >> final-report.md + elif [[ "$RELEASE_RESULT" == "failure" ]]; then + echo "โŒ Release creation failed - check logs for details" >> final-report.md + fi + echo "" >> final-report.md + fi + + # -------------------------------------------------------------------- + # Generate performance insights (collapsed) + # -------------------------------------------------------------------- + - name: ๐Ÿš€ Generate Performance Insights + id: performance-insights + env: + TEST_MATRIX: ${{ inputs.test-matrix }} + run: | + TOTAL_DURATION=${TIMING_total_duration:-0} + TOTAL_MINUTES=${TIMING_total_minutes:-0} + TOTAL_SECONDS=${TIMING_total_seconds:-0} + + { + echo "
" + echo "โฑ๏ธ Performance Insights" + echo "" + } >> final-report.md + + # Overall timing insights + if [[ $TOTAL_DURATION -gt 600 ]]; then + echo "- โš ๏ธ Workflow took longer than 10 minutes (${TOTAL_MINUTES}m ${TOTAL_SECONDS}s)" >> final-report.md + elif [[ $TOTAL_DURATION -gt 300 && $TOTAL_DURATION -le 600 ]]; then + echo "- โ„น๏ธ Workflow completed in ${TOTAL_MINUTES}m ${TOTAL_SECONDS}s" >> final-report.md + elif [[ $TOTAL_DURATION -gt 180 && $TOTAL_DURATION -le 300 ]]; then + echo "- ๐ŸŽ‰ Great: Under 5 minutes (${TOTAL_MINUTES}m ${TOTAL_SECONDS}s)" >> final-report.md + elif [[ $TOTAL_DURATION -le 180 ]]; then + echo "- ๐Ÿš€ Excellent: Under 3 minutes!" >> final-report.md + fi + + # Standard insights + { + echo "- **Parallel Jobs**: Multiple jobs ran in parallel" + echo "- **Matrix Strategy**: $(echo "$TEST_MATRIX" | jq '.include | length') configurations" + } >> final-report.md + + echo "" >> final-report.md + echo "
" >> final-report.md + echo "" >> final-report.md + + # -------------------------------------------------------------------- + # Add compact footer + # -------------------------------------------------------------------- + - name: โœ… Add Report Footer + run: | + { + echo "---" + echo "_๐ŸŽฏ Workflow completed at $(date -u +"%H:%M:%S UTC") โ€” GoFortress CI/CD Pipeline_" + } >> final-report.md + + # -------------------------------------------------------------------- + # Publish final report to GitHub Step Summary + # -------------------------------------------------------------------- + - name: ๐Ÿ“‹ Publish to GitHub Step Summary + run: | + cat final-report.md >> $GITHUB_STEP_SUMMARY + echo "โœ… Completion report generated and published successfully" + + # -------------------------------------------------------------------- + # Upload final report artifact + # -------------------------------------------------------------------- + - name: ๐Ÿ“ค Upload Final Report + if: always() + uses: ./.github/actions/upload-statistics + with: + artifact-name: "final-completion-report" + artifact-path: "final-report.md" + retention-days: "7" diff --git a/.github/workflows/fortress-completion-statistics.yml b/.github/workflows/fortress-completion-statistics.yml deleted file mode 100644 index aaf0e08..0000000 --- a/.github/workflows/fortress-completion-statistics.yml +++ /dev/null @@ -1,722 +0,0 @@ -# ------------------------------------------------------------------------------------ -# Completion Report Statistics Processing (Reusable Workflow) (GoFortress) -# -# Purpose: Process all statistics artifacts for the completion report including -# cache statistics, benchmark results, coverage data, and lines of code summary. -# -# This workflow handles: -# - Cache statistics processing and hit rate analysis -# - Benchmark results and performance metrics -# - Code coverage reporting and badge generation -# - Lines of code calculations and summary -# -# Maintainer: @mrz1836 -# -# ------------------------------------------------------------------------------------ - -name: GoFortress (Completion Statistics) - -on: - workflow_call: - inputs: - timing-metrics: - description: "JSON string of timing data" - required: true - type: string - env-json: - description: "JSON string of environment variables" - required: true - type: string - outputs: - report-section: - description: "Generated statistics markdown section" - value: ${{ jobs.process-statistics.outputs.statistics-markdown }} - cache-metrics: - description: "Cache performance metrics" - value: ${{ jobs.process-statistics.outputs.cache-data }} - benchmark-metrics: - description: "Benchmark performance metrics" - value: ${{ jobs.process-statistics.outputs.benchmark-data }} - coverage-metrics: - description: "Coverage metrics" - value: ${{ jobs.process-statistics.outputs.coverage-data }} - -# Security: Restrict default permissions (jobs must explicitly request what they need) -permissions: {} - -jobs: - # ---------------------------------------------------------------------------------- - # Statistics Processing - # ---------------------------------------------------------------------------------- - process-statistics: - name: ๐Ÿ“Š Process Statistics - runs-on: ubuntu-latest - if: always() - permissions: - contents: read - actions: read - outputs: - statistics-markdown: ${{ steps.set-output.outputs.content }} - cache-data: ${{ steps.process-cache.outputs.cache-metrics }} - benchmark-data: ${{ steps.process-benchmarks.outputs.benchmark-metrics }} - coverage-data: ${{ steps.process-coverage.outputs.coverage-metrics }} - steps: - # -------------------------------------------------------------------- - # Checkout repository for local actions - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - env: - ENV_JSON: ${{ inputs.env-json }} - run: | - echo "๐Ÿ“‹ Setting environment variables..." - echo "$ENV_JSON" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' | while IFS='=' read -r key value; do - echo "$key=$value" >> $GITHUB_ENV - done - - # -------------------------------------------------------------------- - # Detect if this is a release build (tag) - # -------------------------------------------------------------------- - - name: ๐Ÿท๏ธ Detect Release Build - run: | - # Detect if this is a release build - IS_RELEASE_BUILD=false - if [[ "$GITHUB_REF" == refs/tags/* ]]; then - IS_RELEASE_BUILD=true - TAG_NAME="${GITHUB_REF#refs/tags/}" - echo "๐Ÿ“ฆ Detected release build (tag: $TAG_NAME)" - echo "IS_RELEASE_BUILD=true" >> $GITHUB_ENV - echo "RELEASE_TAG=$TAG_NAME" >> $GITHUB_ENV - else - echo "๐Ÿ”„ Regular build (non-release)" - echo "IS_RELEASE_BUILD=false" >> $GITHUB_ENV - fi - - # -------------------------------------------------------------------- - # Download specific artifacts needed for statistics processing - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Download benchmark statistics - if: always() - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "bench-stats-*" - path: ./artifacts/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ“ฅ Download cache statistics - if: always() - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "cache-stats-*" - path: ./artifacts/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ“ฅ Download internal coverage statistics - if: always() && env.ENABLE_GO_TESTS == 'true' && env.GO_COVERAGE_PROVIDER == 'internal' - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "coverage-stats-internal" - path: ./artifacts/ - merge-multiple: false - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: true - - - name: ๐Ÿ“ฅ Download codecov coverage statistics - if: always() && env.ENABLE_GO_TESTS == 'true' && env.GO_COVERAGE_PROVIDER == 'codecov' - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "coverage-stats-codecov" - path: ./artifacts/ - merge-multiple: false - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: true - - - name: ๐Ÿ—‚๏ธ Flatten artifacts - if: always() - run: | - echo "๐Ÿ—‚๏ธ Flattening downloaded artifacts..." - if [ -d "./artifacts/" ]; then - find ./artifacts/ -name "*.json" -type f | while read -r file; do - filename=$(basename "$file") - echo "Moving $file to ./$filename" - cp "$file" "./$filename" - done - echo "๐Ÿ“‹ Available stats files:" - ls -la *-stats-*.json 2>/dev/null || echo "No stats files found" - else - echo "โš ๏ธ No artifacts directory found" - fi - - # -------------------------------------------------------------------- - # Setup MAGE-X for LOC metrics - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Setup MAGE-X for LOC metrics - uses: ./.github/actions/setup-magex - with: - magex-version: ${{ env.MAGE_X_VERSION }} - runner-os: ${{ runner.os }} - use-local: ${{ env.MAGE_X_USE_LOCAL }} - - # -------------------------------------------------------------------- - # Initialize statistics report section - # -------------------------------------------------------------------- - - name: ๐Ÿ“ Initialize Statistics Section - run: | - touch statistics-section.md - - # -------------------------------------------------------------------- - # Process cache statistics - # -------------------------------------------------------------------- - - name: ๐Ÿ’พ Process Cache Statistics - id: process-cache - run: | - # Process cache statistics if available - if compgen -G "cache-stats-*.json" >/dev/null 2>&1; then - { - echo "" - echo "### ๐Ÿ’พ Cache Statistics" - echo "| Workflow/Job | OS | Go Version | Module Cache | Build Cache | Module Size | Build Size | Redis Cache | Redis Size |" - echo "|--------------|----|-----------|--------------|-----------|-----------|------------|-------------|------------|" - } >> statistics-section.md - - TOTAL_CACHE_HITS=0 - TOTAL_CACHE_ATTEMPTS=0 - WORKFLOWS_WITH_CACHE="" - - for stats_file in cache-stats-*.json; do - if [ -f "$stats_file" ]; then - OS=$(jq -r '.os' "$stats_file") - GO_VER=$(jq -r '.go_version' "$stats_file") - WORKFLOW=$(jq -r '.workflow // "unknown"' "$stats_file") - JOB_NAME=$(jq -r '.job_name // ""' "$stats_file") - GOMOD_HIT=$(jq -r '.gomod_cache_hit' "$stats_file") - GOBUILD_HIT=$(jq -r '.gobuild_cache_hit' "$stats_file") - GOMOD_SIZE=$(jq -r '.cache_size_gomod' "$stats_file") - GOBUILD_SIZE=$(jq -r '.cache_size_gobuild' "$stats_file") - - # Redis cache statistics - REDIS_ENABLED=$(jq -r '.redis_enabled // "false"' "$stats_file") - REDIS_HIT=$(jq -r '.redis_cache_hit // "false"' "$stats_file") - REDIS_SIZE=$(jq -r '.redis_image_size_mb // "0"' "$stats_file") - - GOMOD_ICON=$([[ "$GOMOD_HIT" == "true" ]] && echo "โœ… Hit" || echo "โŒ Miss") - GOBUILD_ICON=$([[ "$GOBUILD_HIT" == "true" ]] && echo "โœ… Hit" || echo "โŒ Miss") - - # Redis cache display - if [[ "$REDIS_ENABLED" == "true" ]]; then - REDIS_ICON=$([[ "$REDIS_HIT" == "true" ]] && echo "โœ… Hit" || echo "โŒ Miss") - REDIS_SIZE_DISPLAY="${REDIS_SIZE}MB" - else - REDIS_ICON="โž– N/A" - REDIS_SIZE_DISPLAY="โž–" - fi - - # Create workflow/job identifier - if [[ -n "$JOB_NAME" && "$JOB_NAME" != "null" ]]; then - WORKFLOW_JOB="${WORKFLOW}/${JOB_NAME}" - else - WORKFLOW_JOB="${WORKFLOW}" - fi - - echo "| $WORKFLOW_JOB | $OS | $GO_VER | $GOMOD_ICON | $GOBUILD_ICON | $GOMOD_SIZE | $GOBUILD_SIZE | $REDIS_ICON | $REDIS_SIZE_DISPLAY |" >> statistics-section.md - - [[ "$GOMOD_HIT" == "true" ]] && TOTAL_CACHE_HITS=$((TOTAL_CACHE_HITS + 1)) - [[ "$GOBUILD_HIT" == "true" ]] && TOTAL_CACHE_HITS=$((TOTAL_CACHE_HITS + 1)) - [[ "$REDIS_ENABLED" == "true" && "$REDIS_HIT" == "true" ]] && TOTAL_CACHE_HITS=$((TOTAL_CACHE_HITS + 1)) - - TOTAL_CACHE_ATTEMPTS=$((TOTAL_CACHE_ATTEMPTS + 2)) - [[ "$REDIS_ENABLED" == "true" ]] && TOTAL_CACHE_ATTEMPTS=$((TOTAL_CACHE_ATTEMPTS + 1)) - - # Track workflows that used cache - if [[ "$WORKFLOWS_WITH_CACHE" != *"$WORKFLOW"* ]]; then - if [[ -z "$WORKFLOWS_WITH_CACHE" ]]; then - WORKFLOWS_WITH_CACHE="$WORKFLOW" - else - WORKFLOWS_WITH_CACHE="${WORKFLOWS_WITH_CACHE}, $WORKFLOW" - fi - fi - fi - done - - # Add cache efficiency summary - if [[ $TOTAL_CACHE_ATTEMPTS -gt 0 ]]; then - CACHE_HIT_RATE=$((TOTAL_CACHE_HITS * 100 / TOTAL_CACHE_ATTEMPTS)) - { - echo "" - echo "**Cache Performance Summary:**" - echo "- **Overall Hit Rate**: ${CACHE_HIT_RATE}% (${TOTAL_CACHE_HITS}/${TOTAL_CACHE_ATTEMPTS} cache operations)" - echo "- **Workflows Using Cache**: $WORKFLOWS_WITH_CACHE" - } >> statistics-section.md - - if [[ $CACHE_HIT_RATE -ge 80 ]]; then - echo "- **Cache Efficiency**: ๐Ÿš€ Excellent (${CACHE_HIT_RATE}% hit rate)" >> statistics-section.md - elif [[ $CACHE_HIT_RATE -ge 60 ]]; then - echo "- **Cache Efficiency**: โœ… Good (${CACHE_HIT_RATE}% hit rate)" >> statistics-section.md - elif [[ $CACHE_HIT_RATE -ge 40 ]]; then - echo "- **Cache Efficiency**: โš ๏ธ Fair (${CACHE_HIT_RATE}% hit rate)" >> statistics-section.md - else - echo "- **Cache Efficiency**: โŒ Poor (${CACHE_HIT_RATE}% hit rate - consider optimizing cache strategy)" >> statistics-section.md - fi - - # 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 - - # 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 - - # -------------------------------------------------------------------- - # Process benchmark statistics - # -------------------------------------------------------------------- - - name: ๐Ÿƒ Process Benchmark Statistics - id: process-benchmarks - run: | - # Process benchmark statistics if available - if compgen -G "bench-stats-*.json" >/dev/null 2>&1; then - { - echo "" - echo "" - echo "### โšก Benchmark Results" - } >> statistics-section.md - - # Get benchmark mode from the first stats file - BENCH_MODE="normal" - for stats_file in bench-stats-*.json; do - if [ -f "$stats_file" ]; then - BENCH_MODE=$(jq -r '.benchmark_mode // "normal"' "$stats_file") - break - fi - done - - { - echo "" - echo "**Mode**: \`$BENCH_MODE\` $(case "$BENCH_MODE" in quick) echo "(Quick 50ms runs)" ;; full) echo "(Comprehensive 10s runs)" ;; *) echo "(Normal 100ms runs)" ;; esac)" - echo "" - echo "| Benchmark Suite | Duration | Benchmarks | Status |" - echo "|-----------------|----------|------------|--------|" - } >> statistics-section.md - - TOTAL_BENCHMARKS=0 - TOTAL_DURATION=0 - - for stats_file in bench-stats-*.json; do - if [ -f "$stats_file" ]; then - NAME=$(jq -r '.name' "$stats_file") - DURATION=$(jq -r '.duration_seconds' "$stats_file") - BENCHMARK_COUNT=$(jq -r '.benchmark_count' "$stats_file") - STATUS=$(jq -r '.status' "$stats_file") - - DURATION_MIN=$((DURATION / 60)) - DURATION_SEC=$((DURATION % 60)) - STATUS_ICON=$([[ "$STATUS" == "success" ]] && echo "โœ…" || echo "โŒ") - - echo "| $NAME | ${DURATION_MIN}m ${DURATION_SEC}s | $BENCHMARK_COUNT | $STATUS_ICON |" >> statistics-section.md - - TOTAL_BENCHMARKS=$((TOTAL_BENCHMARKS + BENCHMARK_COUNT)) - TOTAL_DURATION=$((TOTAL_DURATION + DURATION)) - fi - done - - # Display detailed benchmark results - { - echo "" - echo "
" - echo "Detailed Benchmark Results" - echo "" - } >> statistics-section.md - - for stats_file in bench-stats-*.json; do - if [ -f "$stats_file" ]; then - NAME=$(jq -r '.name' "$stats_file") - BENCHMARK_SUMMARY=$(jq -r '.benchmark_summary' "$stats_file") - if [ -n "$BENCHMARK_SUMMARY" ] && [ "$BENCHMARK_SUMMARY" != "null" ]; then - { - echo "#### $NAME" - echo "$BENCHMARK_SUMMARY" - echo "" - } >> statistics-section.md - fi - fi - done - - echo "


" >> statistics-section.md - - # 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 - - # -------------------------------------------------------------------- - # Process coverage statistics - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Process Coverage Statistics - id: process-coverage - run: | - # Process coverage statistics if available - echo "๐Ÿ” Looking for coverage statistics files..." - - # Check for both coverage-stats-*.json and coverage-stats-internal-*.json patterns - COVERAGE_FILES_FOUND=false - if compgen -G "coverage-stats-*.json" >/dev/null 2>&1; then - echo "๐Ÿ“‹ Found coverage-stats-*.json files:" - ls -la coverage-stats-*.json || echo "None" - COVERAGE_FILES_FOUND=true - fi - - if compgen -G "coverage-stats-internal-*.json" >/dev/null 2>&1; then - echo "๐Ÿ“‹ Found coverage-stats-internal-*.json files:" - ls -la coverage-stats-internal-*.json || echo "None" - COVERAGE_FILES_FOUND=true - fi - - if [[ "$COVERAGE_FILES_FOUND" == "true" ]]; then - # Check if we have valid coverage data before creating section - HAS_COVERAGE_DATA=false - VALID_COVERAGE_FILE="" - - # Check both patterns for valid coverage data, prioritizing updated files - UPDATED_FILE_FOUND=false - for pattern in "coverage-stats-*.json" "coverage-stats-internal-*.json"; do - if compgen -G "$pattern" >/dev/null 2>&1; then - # First, prioritize any "updated" statistics files - for stats_file in $pattern; do - if [ -f "$stats_file" ] && [[ "$stats_file" == *"updated"* ]]; then - echo "๐Ÿ” Checking UPDATED statistics file: $stats_file" - COVERAGE_PERCENT=$(jq -r '.coverage_percent // .coverage_percentage // "null"' "$stats_file") - echo " - Coverage value found: '$COVERAGE_PERCENT'" - - if [[ "$COVERAGE_PERCENT" != "null" ]] && [[ "$COVERAGE_PERCENT" != "N/A" ]] && [[ -n "$COVERAGE_PERCENT" ]]; then - echo "โœ… Valid coverage data found in UPDATED file: $stats_file" - HAS_COVERAGE_DATA=true - VALID_COVERAGE_FILE="$stats_file" - UPDATED_FILE_FOUND=true - break 2 - fi - fi - done - - # If no updated file found, check regular files - if [[ "$UPDATED_FILE_FOUND" == "false" ]]; then - for stats_file in $pattern; do - if [ -f "$stats_file" ] && [[ "$stats_file" != *"updated"* ]]; then - echo "๐Ÿ” Checking $stats_file for valid coverage data..." - # Try both field names: coverage_percent and coverage_percentage - COVERAGE_PERCENT=$(jq -r '.coverage_percent // .coverage_percentage // "null"' "$stats_file") - echo " - Coverage value found: '$COVERAGE_PERCENT'" - - if [[ "$COVERAGE_PERCENT" != "null" ]] && [[ "$COVERAGE_PERCENT" != "N/A" ]] && [[ -n "$COVERAGE_PERCENT" ]]; then - echo "โœ… Valid coverage data found in $stats_file" - HAS_COVERAGE_DATA=true - VALID_COVERAGE_FILE="$stats_file" - break 2 # Break out of both loops - else - echo "โš ๏ธ No valid coverage data in $stats_file" - fi - fi - done - fi - fi - done - - if [[ "$HAS_COVERAGE_DATA" == "true" ]] && [[ -n "$VALID_COVERAGE_FILE" ]]; then - { - echo "" - echo "

" - echo "" - echo "### ๐Ÿ“ˆ Code Coverage Report" - } >> statistics-section.md - - # Process the valid coverage file - echo "๐Ÿ“Š Processing coverage data from: $VALID_COVERAGE_FILE" - - # Extract coverage percentage (try both field names) - COVERAGE_PERCENT=$(jq -r '.coverage_percent // .coverage_percentage // "N/A"' "$VALID_COVERAGE_FILE") - PROCESSING_TIME=$(jq -r '.processing_time_seconds // "N/A"' "$VALID_COVERAGE_FILE") - FILES_PROCESSED=$(jq -r '.files_processed // "N/A"' "$VALID_COVERAGE_FILE") - BADGE_GENERATED=$(jq -r '.badge_generated // "false"' "$VALID_COVERAGE_FILE") - PAGES_DEPLOYED=$(jq -r '.pages_deployed // "false"' "$VALID_COVERAGE_FILE") - COVERAGE_PROVIDER=$(jq -r '.provider // "N/A"' "$VALID_COVERAGE_FILE") - - echo "๐Ÿ“‹ Coverage metrics: ${COVERAGE_PERCENT}%, ${FILES_PROCESSED} files, ${PROCESSING_TIME}s processing" - - { - echo "| Metric | Value |" - echo "|--------|-------|" - echo "| **Coverage Percentage** | $COVERAGE_PERCENT% |" - echo "| **Processing Time** | ${PROCESSING_TIME}s |" - echo "| **Files Processed** | $FILES_PROCESSED |" - echo "| **Coverage Provider** | $([ "$COVERAGE_PROVIDER" == "internal" ] && echo "go-coverage" || ([ "$COVERAGE_PROVIDER" == "codecov" ] && echo "Codecov" || echo "$COVERAGE_PROVIDER")) |" - echo "| **Badge Generated** | $([ "$BADGE_GENERATED" == "true" ] && echo "โœ… Yes" || echo "โŒ No") |" - echo "| **Pages Deployed** | $([ "$PAGES_DEPLOYED" == "true" ] && echo "โœ… Yes" || echo "โŒ No") |" - } >> statistics-section.md - - # Store metrics for output - echo "coverage-metrics={\"percentage\":\"$COVERAGE_PERCENT\",\"files_processed\":\"$FILES_PROCESSED\",\"processing_time\":\"$PROCESSING_TIME\",\"provider\":\"$COVERAGE_PROVIDER\"}" >> $GITHUB_OUTPUT - fi - elif [[ "${{ env.ENABLE_CODE_COVERAGE }}" == "true" ]]; then - # Coverage is enabled but no coverage data found - show status - { - echo "" - echo "

" - echo "" - echo "### ๐Ÿ“ˆ Code Coverage Status" - echo "| Status | Details |" - echo "|--------|---------|" - - # Show different message for release builds vs regular builds - if [[ "${IS_RELEASE_BUILD:-false}" == "true" ]]; then - echo "| **Coverage** | ๐Ÿ“ฆ Coverage analysis skipped for release builds |" - echo "| **Reason** | Coverage processing is disabled during releases to optimize build time |" - if [[ -n "${RELEASE_TAG:-}" ]]; then - echo "| **Release Tag** | \`${RELEASE_TAG}\` |" - fi - else - echo "| **Coverage** | โš ๏ธ No coverage data available - check job logs |" - fi - - echo "| **Threshold** | ${{ env.GO_COVERAGE_THRESHOLD }}% minimum |" - echo "| **Badge Style** | ${{ env.GO_COVERAGE_BADGE_STYLE }} |" - echo "| **PR Comments** | $([ "${{ env.GO_COVERAGE_POST_COMMENTS }}" == "true" ] && echo "โœ… Enabled" || echo "โŒ Disabled") |" - echo "| **Theme** | ${{ env.GO_COVERAGE_REPORT_THEME }} |" - } >> statistics-section.md - fi - - # -------------------------------------------------------------------- - # Generate Lines of Code Summary - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Generate Lines of Code Summary - id: process-loc - run: | - echo "๐Ÿ“Š Running magex metrics:loc json..." - - # Run magex metrics:loc json and capture output - LOC_OUTPUT=$(magex metrics:loc json 2>&1 || true) - LOC_FOUND=false - - # Save raw JSON for loc-stats artifact (consumed by go-broadcast analytics) - if [[ -n "$LOC_OUTPUT" ]] && echo "$LOC_OUTPUT" | jq empty 2>/dev/null; then - echo "$LOC_OUTPUT" > loc-stats.json - echo "๐Ÿ“ฆ Saved loc-stats.json for artifact upload" - fi - - if [[ -n "$LOC_OUTPUT" ]]; then - echo "๐Ÿ“‹ magex metrics:loc json output:" - echo "$LOC_OUTPUT" - - # Parse JSON output using jq - TEST_FILES_LOC=$(echo "$LOC_OUTPUT" | jq -r '.test_files_loc // empty') - TEST_FILES_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.test_files_count // empty') - GO_FILES_LOC=$(echo "$LOC_OUTPUT" | jq -r '.go_files_loc // empty') - GO_FILES_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.go_files_count // empty') - TOTAL_LOC=$(echo "$LOC_OUTPUT" | jq -r '.total_loc // empty') - TOTAL_FILES_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.total_files_count // empty') - LOC_DATE=$(echo "$LOC_OUTPUT" | jq -r '.date // empty') - - # Parse new size metrics - TEST_FILES_SIZE=$(echo "$LOC_OUTPUT" | jq -r '.test_files_size_human // empty') - GO_FILES_SIZE=$(echo "$LOC_OUTPUT" | jq -r '.go_files_size_human // empty') - TOTAL_SIZE=$(echo "$LOC_OUTPUT" | jq -r '.total_size_human // empty') - TEST_AVG_SIZE_BYTES=$(echo "$LOC_OUTPUT" | jq -r '.test_avg_size_bytes // empty') - GO_AVG_SIZE_BYTES=$(echo "$LOC_OUTPUT" | jq -r '.go_avg_size_bytes // empty') - - # Parse code quality metrics - AVG_LINES_PER_FILE=$(echo "$LOC_OUTPUT" | jq -r '.avg_lines_per_file // empty') - TEST_COVERAGE_RATIO=$(echo "$LOC_OUTPUT" | jq -r '.test_coverage_ratio // empty') - PACKAGE_COUNT=$(echo "$LOC_OUTPUT" | jq -r '.package_count // empty') - - echo " - Test Files LOC: '$TEST_FILES_LOC' (count: $TEST_FILES_COUNT)" - echo " - Go Files LOC: '$GO_FILES_LOC' (count: $GO_FILES_COUNT)" - echo " - Total LOC: '$TOTAL_LOC' (files: $TOTAL_FILES_COUNT)" - echo " - Date: '$LOC_DATE'" - - # Check if we have valid LOC data - if [[ -n "$TEST_FILES_LOC" ]] && [[ -n "$GO_FILES_LOC" ]] && [[ -n "$TOTAL_LOC" ]]; then - LOC_FOUND=true - echo "โœ… Successfully parsed LOC JSON data" - - # Optionally warn if new metrics are missing - if [[ -z "$TOTAL_SIZE" ]] || [[ -z "$AVG_LINES_PER_FILE" ]]; then - echo "โš ๏ธ Some enhanced metrics are missing (older magex version?)" - fi - fi - else - echo "โš ๏ธ No output from magex metrics:loc json" - fi - - # Display LOC section - if [[ "$LOC_FOUND" == "true" ]]; then - # Format numbers with commas for display - DISPLAY_TEST_LOC=$(LC_NUMERIC=en_US.UTF-8 printf "%'d" "${TEST_FILES_LOC:-0}") - DISPLAY_TEST_COUNT="${TEST_FILES_COUNT:-N/A}" - DISPLAY_GO_LOC=$(LC_NUMERIC=en_US.UTF-8 printf "%'d" "${GO_FILES_LOC:-0}") - DISPLAY_GO_COUNT="${GO_FILES_COUNT:-N/A}" - DISPLAY_TOTAL_LOC=$(LC_NUMERIC=en_US.UTF-8 printf "%'d" "${TOTAL_LOC:-0}") - DISPLAY_TOTAL_FILES="${TOTAL_FILES_COUNT:-N/A}" - DISPLAY_LOC_DATE="${LOC_DATE:-N/A}" - - # Format average sizes for display - if [[ -n "$TEST_AVG_SIZE_BYTES" ]] && [[ "$TEST_AVG_SIZE_BYTES" != "0" ]]; then - DISPLAY_TEST_AVG_SIZE=$(numfmt --to=iec-i --suffix=B "$TEST_AVG_SIZE_BYTES" 2>/dev/null || echo "${TEST_AVG_SIZE_BYTES}B") - else - DISPLAY_TEST_AVG_SIZE="N/A" - fi - - if [[ -n "$GO_AVG_SIZE_BYTES" ]] && [[ "$GO_AVG_SIZE_BYTES" != "0" ]]; then - DISPLAY_GO_AVG_SIZE=$(numfmt --to=iec-i --suffix=B "$GO_AVG_SIZE_BYTES" 2>/dev/null || echo "${GO_AVG_SIZE_BYTES}B") - else - DISPLAY_GO_AVG_SIZE="N/A" - fi - - DISPLAY_TEST_SIZE="${TEST_FILES_SIZE:-N/A}" - DISPLAY_GO_SIZE="${GO_FILES_SIZE:-N/A}" - DISPLAY_TOTAL_SIZE="${TOTAL_SIZE:-N/A}" - - { - echo "" - echo "

" - echo "" - echo "### ๐Ÿ“Š Lines of Code Summary" - echo "| Type | Lines of Code | Files | Total Size | Avg Size | Date |" - echo "|------|---------------|-------|------------|----------|------|" - echo "| Test Files | $DISPLAY_TEST_LOC | $DISPLAY_TEST_COUNT | $DISPLAY_TEST_SIZE | $DISPLAY_TEST_AVG_SIZE | $DISPLAY_LOC_DATE |" - echo "| Go Files | $DISPLAY_GO_LOC | $DISPLAY_GO_COUNT | $DISPLAY_GO_SIZE | $DISPLAY_GO_AVG_SIZE | $DISPLAY_LOC_DATE |" - echo "| **Total** | **$DISPLAY_TOTAL_LOC** | **$DISPLAY_TOTAL_FILES** | **$DISPLAY_TOTAL_SIZE** | | |" - echo "" - - # Display code quality metrics if available - if [[ -n "$AVG_LINES_PER_FILE" ]] || [[ -n "$TEST_COVERAGE_RATIO" ]] || [[ -n "$PACKAGE_COUNT" ]]; then - echo "#### ๐Ÿ“ˆ Code Quality Metrics" - echo "" - echo "| Metric | Value |" - echo "|--------|-------|" - - # Display average lines per file - if [[ -n "$AVG_LINES_PER_FILE" ]]; then - DISPLAY_AVG_LINES=$(LC_NUMERIC=en_US.UTF-8 printf "%.1f" "${AVG_LINES_PER_FILE}") - echo "| Average Lines per File | $DISPLAY_AVG_LINES |" - fi - - # Display test coverage ratio - if [[ -n "$TEST_COVERAGE_RATIO" ]]; then - DISPLAY_COVERAGE=$(LC_NUMERIC=en_US.UTF-8 printf "%.1f%%" "${TEST_COVERAGE_RATIO}") - echo "| Test Coverage Ratio | $DISPLAY_COVERAGE |" - fi - - # Display package count - if [[ -n "$PACKAGE_COUNT" ]]; then - echo "| Package/Directory Count | $PACKAGE_COUNT |" - fi - - # Display total size - if [[ -n "$TOTAL_SIZE" ]]; then - echo "| Total Project Size | $TOTAL_SIZE |" - fi - - echo "" - fi - - echo "

" - } >> statistics-section.md - - echo "โœ… LOC section added: Test=$DISPLAY_TEST_LOC ($DISPLAY_TEST_COUNT files), Go=$DISPLAY_GO_LOC ($DISPLAY_GO_COUNT files), Total=$DISPLAY_TOTAL_LOC ($DISPLAY_TOTAL_FILES files)" - else - echo "โš ๏ธ Could not collect LOC data" - { - echo "" - echo "

" - echo "" - echo "### ๐Ÿ“Š Lines of Code Summary" - echo "| Status | Details |" - echo "|--------|---------|" - echo "| **Lines of Code** | โŒ Data not available |" - echo "| **Reason** | magex metrics:loc json command failed or produced unexpected output |" - echo "" - echo "

" - } >> statistics-section.md - fi - - # -------------------------------------------------------------------- - # Upload statistics section - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload LOC Stats JSON - if: always() && hashFiles('loc-stats.json') != '' - uses: ./.github/actions/upload-artifact-resilient - with: - artifact-name: loc-stats - artifact-path: loc-stats.json - retention-days: "7" - - - name: ๐Ÿ“ค Upload Statistics Section - id: upload-section - if: always() - run: | - if [ -f "statistics-section.md" ] && [ -s "statistics-section.md" ]; then - echo "๐Ÿ“Š Statistics section found, uploading..." - ls -la statistics-section.md - echo "๐Ÿ“‹ Content preview:" - head -5 statistics-section.md - else - echo "โš ๏ธ Statistics section file missing or empty, creating minimal section..." - echo "### ๐Ÿ“Š Statistics Section" > statistics-section.md - echo "No statistics data available for this run." >> statistics-section.md - fi - - - name: ๐Ÿ“ค Upload Statistics Artifact - uses: ./.github/actions/upload-statistics - with: - artifact-name: "statistics-section" - artifact-path: "statistics-section.md" - retention-days: "7" - if-no-files-found: "warn" - - - name: ๐Ÿ“‹ Set Output Content - id: set-output - run: | - echo "content<> $GITHUB_OUTPUT - cat statistics-section.md >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/fortress-completion-tests.yml b/.github/workflows/fortress-completion-tests.yml deleted file mode 100644 index 6a5bea5..0000000 --- a/.github/workflows/fortress-completion-tests.yml +++ /dev/null @@ -1,476 +0,0 @@ -# ------------------------------------------------------------------------------------ -# Completion Report Test Analysis (Reusable Workflow) (GoFortress) -# -# Purpose: Process all test-related artifacts for the completion report including -# test results, test configuration analysis, and fuzz testing results. -# -# This workflow handles: -# - Test statistics processing and failure analysis -# - Test configuration and output mode analysis -# - Fuzz test statistics and security testing results -# - Test failure details and error extraction -# -# Maintainer: @mrz1836 -# -# ------------------------------------------------------------------------------------ - -name: GoFortress (Completion Tests) - -on: - workflow_call: - inputs: - test-suite-result: - description: "Result of the test suite job" - required: true - type: string - env-json: - description: "JSON string of environment variables" - required: true - type: string - outputs: - report-section: - description: "Generated test analysis markdown section" - value: ${{ jobs.analyze-tests.outputs.tests-markdown }} - test-metrics: - description: "Test performance metrics" - value: ${{ jobs.analyze-tests.outputs.test-data }} - failure-metrics: - description: "Test failure analysis metrics" - value: ${{ jobs.analyze-tests.outputs.failure-data }} - -# Security: Restrict default permissions (jobs must explicitly request what they need) -permissions: {} - -jobs: - # ---------------------------------------------------------------------------------- - # Test Analysis - # ---------------------------------------------------------------------------------- - analyze-tests: - name: ๐Ÿงช Analyze Test Results - runs-on: ubuntu-latest - if: always() - permissions: - contents: read - actions: read - outputs: - tests-markdown: ${{ steps.set-output.outputs.content }} - test-data: ${{ steps.process-tests.outputs.test-metrics }} - failure-data: ${{ steps.process-tests.outputs.failure-metrics }} - steps: - # -------------------------------------------------------------------- - # Checkout repository for local actions - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - env: - ENV_JSON: ${{ inputs.env-json }} - run: | - echo "๐Ÿ“‹ Setting environment variables..." - echo "$ENV_JSON" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' | while IFS='=' read -r key value; do - echo "$key=$value" >> $GITHUB_ENV - done - - # -------------------------------------------------------------------- - # Download specific artifacts needed for test analysis - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Download benchmark statistics - if: always() - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "bench-stats-*" - path: ./artifacts/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ“ฅ Download cache statistics - if: always() - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "cache-stats-*" - path: ./artifacts/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ“ฅ Download fuzz test failure artifacts - if: always() && env.ENABLE_GO_TESTS == 'true' && env.ENABLE_FUZZ_TESTING == 'true' - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "test-results-fuzz-*" - path: ./test-artifacts/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ“ฅ Download CI results (native mode) - if: always() && env.ENABLE_GO_TESTS == 'true' - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "ci-results-*" - path: ./ci-artifacts/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - - name: ๐Ÿ—‚๏ธ Flatten artifacts - if: always() - run: | - echo "๐Ÿ—‚๏ธ Flattening downloaded artifacts..." - - # Source shared helper functions for artifact processing - source .github/scripts/parse-test-label.sh || { echo "โŒ Failed to source parse-test-label.sh"; exit 1; } - - # Verify critical function is available - if ! type copy_ci_artifact &>/dev/null; then - echo "โŒ Error: copy_ci_artifact function not found after sourcing" - exit 1 - fi - - # Process stats artifacts (bench-stats, cache-stats JSON files) - if [ -d "./artifacts/" ]; then - find ./artifacts/ -name "*.json" -type f | while read -r file; do - filename=$(basename "$file") - echo "Moving $file to ./$filename" - cp "$file" "./$filename" - done - echo "๐Ÿ“‹ Available stats files:" - ls -la *-stats-*.json 2>/dev/null || echo "No stats files found" - else - echo "โ„น๏ธ No artifacts directory found" - fi - - # Process CI results from ci-artifacts (unit tests) - if [ -d "./ci-artifacts/" ]; then - echo "๐Ÿ“‹ Processing unit test CI results..." - while IFS= read -r -d '' file; do - copy_ci_artifact "$file" "ci" || true - done < <(find ./ci-artifacts/ -name "*.jsonl" -type f -print0 2>/dev/null) - fi - - # Process CI results from test-artifacts (fuzz tests) - if [ -d "./test-artifacts/" ]; then - echo "๐Ÿ“‹ Processing fuzz test CI results..." - while IFS= read -r -d '' file; do - copy_ci_artifact "$file" "ci" || true - done < <(find ./test-artifacts/ -name "*.jsonl" -type f -print0 2>/dev/null) - fi - - # Show all available JSONL files - echo "๐Ÿ“‹ Available CI results JSONL files:" - ls -la ci-*.jsonl 2>/dev/null || echo "No CI results JSONL files found" - - # -------------------------------------------------------------------- - # Initialize test analysis section - # -------------------------------------------------------------------- - - name: ๐Ÿ“ Initialize Test Analysis Section - run: | - touch tests-section.md - - # -------------------------------------------------------------------- - # Process test statistics - # -------------------------------------------------------------------- - - name: ๐Ÿงช Process Test Statistics - id: process-tests - run: | - # Source shared helper function for generating test labels - source .github/scripts/parse-test-label.sh || { echo "โŒ Failed to source parse-test-label.sh"; exit 1; } - - # Enable nullglob so "for f in *.jsonl" loops safely skip when no files match - # (prevents iterating with literal pattern string "ci-*.jsonl") - shopt -s nullglob - - # Initialize totals for summary - TOTAL_TESTS=0 - TOTAL_FAILURES=0 - TOTAL_PASSED=0 - TOTAL_SKIPPED=0 - SUITE_COUNT=0 - HAS_DATA=false - - # Check for native CI mode JSONL files first (preferred) - if compgen -G "ci-*.jsonl" >/dev/null 2>&1; then - echo "๐Ÿ“Š Processing native CI mode JSONL files..." - HAS_DATA=true - - { - echo "" - echo "" - echo "### ๐Ÿงช Test Results Summary" - echo "| Test Suite | Duration | Tests | Runs | Passed | Failed | Skipped | Status |" - echo "|------------|----------|-------|------|--------|--------|---------|--------|" - } >> tests-section.md - - # Process each JSONL file - for jsonl_file in ci-*.jsonl; do - if [ -f "$jsonl_file" ]; then - # Extract artifact name from filename (ci-ARTIFACT_NAME-ci-results.jsonl) - ARTIFACT_NAME=$(echo "$jsonl_file" | sed 's/^ci-//' | sed 's/-ci-results\.jsonl$//') - SUITE_LABEL=$(parse_test_label "$ARTIFACT_NAME") - - # Extract summary line - SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") - - if [[ -n "$SUMMARY" ]]; then - STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') - PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') - FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') - SKIPPED=$(echo "$SUMMARY" | jq -r '.summary.skipped // 0') - TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') - UNIQUE=$(echo "$SUMMARY" | jq -r '.summary.unique_total // .summary.total // 0') - DURATION=$(echo "$SUMMARY" | jq -r '.summary.duration // "0s"') - - STATUS_ICON=$([[ "$STATUS" == "passed" ]] && echo "โœ…" || echo "โŒ") - - echo "| $SUITE_LABEL | $DURATION | $UNIQUE | $TOTAL | $PASSED | $FAILED | $SKIPPED | $STATUS_ICON |" >> tests-section.md - - # Accumulate totals (use unique for primary test count) - TOTAL_TESTS=$((TOTAL_TESTS + UNIQUE)) - TOTAL_PASSED=$((TOTAL_PASSED + PASSED)) - TOTAL_FAILURES=$((TOTAL_FAILURES + FAILED)) - TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED)) - SUITE_COUNT=$((SUITE_COUNT + 1)) - fi - fi - done - - # Store totals as outputs - echo "test-metrics={\"total_tests\":$TOTAL_TESTS,\"total_failures\":$TOTAL_FAILURES,\"suite_count\":$SUITE_COUNT}" >> $GITHUB_OUTPUT - - # Add failure analysis if any failures exist - if [[ $TOTAL_FAILURES -gt 0 ]]; then - { - echo "" - echo "" - echo "### โŒ Test Failure Analysis" - echo "**Total Failures**: $TOTAL_FAILURES across $SUITE_COUNT test suite(s)" - echo "" - } >> tests-section.md - - # Show failures by suite - echo "#### ๐Ÿ“Š Failures by Test Suite:" >> tests-section.md - for jsonl_file in ci-*.jsonl; do - if [ -f "$jsonl_file" ]; then - ARTIFACT_NAME=$(echo "$jsonl_file" | sed 's/^ci-//' | sed 's/-ci-results\.jsonl$//') - SUITE_LABEL=$(parse_test_label "$ARTIFACT_NAME") - SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") - - if [[ -n "$SUMMARY" ]]; then - FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') - if [[ $FAILED -gt 0 ]]; then - echo "- **$SUITE_LABEL**: $FAILED failures" >> tests-section.md - fi - fi - fi - done - - # Add collapsible section for failed tests - { - echo "" - echo "
" - echo "๐Ÿ” Failed Tests (click to expand)" - echo "" - echo "| Test Name | Package | Error |" - echo "|-----------|---------|-------|" - } >> tests-section.md - - # Extract failure details from all JSONL files - FAILURE_COUNT=0 - for jsonl_file in ci-*.jsonl; do - if [ -f "$jsonl_file" ] && [[ $FAILURE_COUNT -lt 20 ]]; then - while read -r line; do - if [[ $FAILURE_COUNT -ge 20 ]]; then - break - fi - - TEST=$(echo "$line" | jq -r '.failure.test // "unknown"') - PKG=$(echo "$line" | jq -r '.failure.package // "unknown"' | sed 's|.*/||') - ERROR=$(echo "$line" | jq -r '.failure.error // ""' | head -c 100 | tr '\n' ' ') - - # Truncate error message for table display (max 80 chars: 77 + "...") - if [[ ${#ERROR} -gt 80 ]]; then - ERROR="${ERROR:0:77}..." - fi - - echo "| \`$TEST\` | $PKG | ${ERROR:-_no message_} |" - FAILURE_COUNT=$((FAILURE_COUNT + 1)) - done < <(grep '"type":"failure"' "$jsonl_file" 2>/dev/null) >> tests-section.md || true - fi - done - - { - echo "" - echo "
" - } >> tests-section.md - - # Store failure metrics - echo "failure-metrics={\"total_failures\":$TOTAL_FAILURES,\"has_error_output\":true}" >> $GITHUB_OUTPUT - fi - fi - - # No test statistics available - if [[ "$HAS_DATA" == "false" ]]; then - { - echo "" - echo "" - echo "### ๐Ÿงช Test Results Summary" - echo "" - echo "| Status | Details |" - echo "|--------|---------|" - if [[ "${{ env.ENABLE_GO_TESTS }}" == "false" ]]; then - echo "| **Test Suite** | โŒ Disabled - Set ENABLE_GO_TESTS=true to enable |" - echo "| **Reason** | Tests are disabled via configuration flag |" - echo "| **Note** | Enable ENABLE_GO_TESTS in .github/env/00-core.env to run tests |" - else - 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._" - fi - } >> tests-section.md - fi - - # -------------------------------------------------------------------- - # Add test configuration and output analysis - # -------------------------------------------------------------------- - - name: ๐ŸŽ›๏ธ Add Test Configuration Section - id: add-test-config - run: | - # Add test output configuration section - HAS_CONFIG_DATA=false - - # Check for native CI mode JSONL files - if compgen -G "ci-*.jsonl" >/dev/null 2>&1; then - HAS_CONFIG_DATA=true - { - echo "" - echo "

" - echo "" - echo "### ๐ŸŽ›๏ธ Test Output Configuration" - echo "" - echo "**Output Mode**: Native CI Mode (JSONL)" - echo "" - echo "- Tests executed with magex native CI mode" - echo "- Structured output in .mage-x/ci-results.jsonl" - echo "- Automatic GitHub annotations for failures" - } >> tests-section.md - fi - - if [[ "$HAS_CONFIG_DATA" == "false" ]]; then - # 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 - - # -------------------------------------------------------------------- - # Process fuzz test statistics - # -------------------------------------------------------------------- - - name: ๐ŸŽฏ Process Fuzz Test Statistics - id: process-fuzz - run: | - # Process fuzz test statistics - always show status - { - echo "

" - echo "" - echo "### ๐Ÿ›ก๏ธ Security Testing Results" - } >> tests-section.md - - # Check if fuzz testing is enabled in environment - if [[ "${{ env.ENABLE_FUZZ_TESTING }}" == "true" ]]; then - # Look for fuzz test JSONL files (native CI mode) - FUZZ_JSONL=$(ls ci-*-ci-results-fuzz.jsonl 2>/dev/null | head -1 || echo "") - - if [[ -n "$FUZZ_JSONL" ]] && [[ -f "$FUZZ_JSONL" ]]; then - # Extract summary from fuzz JSONL - SUMMARY=$(grep '"type":"summary"' "$FUZZ_JSONL" 2>/dev/null | head -1 || echo "") - - if [[ -n "$SUMMARY" ]]; then - STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') - TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') - DURATION=$(echo "$SUMMARY" | jq -r '.summary.duration // "0s"') - - STATUS_ICON=$([[ "$STATUS" == "passed" ]] && echo "โœ…" || echo "โŒ") - - # Create table with fuzz test data from JSONL - { - echo "| Fuzz Suite | Duration | Fuzz Tests | Status | Enabled |" - echo "|------------|----------|------------|--------|---------|" - echo "| Fuzz Tests | $DURATION | $TOTAL | $STATUS_ICON | ๐ŸŽฏ |" - } >> tests-section.md - else - # JSONL found but no summary record - { - echo "| Status | Details |" - echo "|--------|---------|" - echo "| **Fuzz Testing** | โœ… Enabled |" - echo "| **Execution** | โš ๏ธ No fuzz summary found in JSONL - check job logs |" - echo "| **Platform** | Linux with primary Go version |" - } >> tests-section.md - fi - else - # No fuzz JSONL found - { - echo "| Status | Details |" - echo "|--------|---------|" - echo "| **Fuzz Testing** | โœ… Enabled |" - echo "| **Execution** | โš ๏ธ No fuzz results found - check job logs |" - echo "| **Platform** | Linux with primary Go version |" - } >> tests-section.md - fi - else - # Fuzz testing is disabled - { - echo "| Status | Details |" - echo "|--------|---------|" - echo "| **Fuzz Testing** | โŒ Disabled |" - echo "| **Configuration** | Set ENABLE_FUZZ_TESTING=true to enable |" - echo "| **Target Platform** | Would run on Linux with primary Go version |" - } >> tests-section.md - fi - - # -------------------------------------------------------------------- - # Upload test analysis section - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload Test Analysis Section - id: upload-section - if: always() - run: | - if [ -f "tests-section.md" ] && [ -s "tests-section.md" ]; then - echo "๐Ÿงช Test section found, uploading..." - ls -la tests-section.md - echo "๐Ÿ“‹ Content preview:" - head -5 tests-section.md - else - echo "โš ๏ธ Test section file missing or empty, creating minimal section..." - echo "### ๐Ÿงช Test Results Section" > tests-section.md - echo "No test data available for this run." >> tests-section.md - fi - - - name: ๐Ÿ“ค Upload Test Artifact - uses: ./.github/actions/upload-statistics - with: - artifact-name: "tests-section" - artifact-path: "tests-section.md" - retention-days: "7" - if-no-files-found: "warn" - - - name: ๐Ÿ“‹ Set Output Content - id: set-output - run: | - echo "content<> $GITHUB_OUTPUT - cat tests-section.md >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/fortress-coverage.yml b/.github/workflows/fortress-coverage.yml index 4f77bbf..af63291 100644 --- a/.github/workflows/fortress-coverage.yml +++ b/.github/workflows/fortress-coverage.yml @@ -44,6 +44,10 @@ on: description: "Path to go.sum file for dependency verification" required: true type: string + coverage-provider: + description: "Coverage service provider (internal or codecov). Gates which downstream job runs." + required: true + type: string secrets: github-token: description: "GitHub token for API access" @@ -60,53 +64,43 @@ permissions: {} jobs: # ---------------------------------------------------------------------------------- - # Check Coverage Provider + # Validate Coverage Provider (fail-fast guard) + # + # Both coverage jobs below are gated on `coverage-provider == 'internal' | 'codecov'`. + # Without this guard, an invalid value would cause BOTH to skip and the reusable + # workflow to report success while silently processing no coverage. This guard provides + # an explicit failure path for any direct caller. # ---------------------------------------------------------------------------------- - check-provider: - name: ๐Ÿ” Check Coverage Provider - runs-on: ubuntu-latest - outputs: - provider: ${{ steps.check.outputs.provider }} - should-run-internal: ${{ steps.check.outputs.should-run-internal }} - should-run-codecov: ${{ steps.check.outputs.should-run-codecov }} + validate-provider: + name: ๐Ÿ” Validate Coverage Provider + runs-on: ${{ inputs.primary-runner }} + timeout-minutes: 2 + permissions: {} steps: - - name: ๐Ÿ”ง Parse environment variables + - name: โœ… Validate coverage-provider input env: - ENV_JSON: ${{ inputs.env-json }} + COVERAGE_PROVIDER: ${{ inputs.coverage-provider }} run: | - echo "$ENV_JSON" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' | while IFS='=' read -r key value; do - echo "$key=$value" >> $GITHUB_ENV - done - - - name: ๐Ÿ” Check coverage provider - id: check - run: | - echo "๐Ÿ” Checking coverage provider configuration..." - PROVIDER="${{ env.GO_COVERAGE_PROVIDER }}" - echo "provider=$PROVIDER" >> $GITHUB_OUTPUT - - if [[ "$PROVIDER" == "internal" ]]; then - echo "โœ… Coverage provider is 'internal' - will use go-coverage with GitHub Pages" - echo "should-run-internal=true" >> $GITHUB_OUTPUT - echo "should-run-codecov=false" >> $GITHUB_OUTPUT - elif [[ "$PROVIDER" == "codecov" ]]; then - echo "โœ… Coverage provider is 'codecov' - will upload to Codecov service" - echo "should-run-internal=false" >> $GITHUB_OUTPUT - echo "should-run-codecov=true" >> $GITHUB_OUTPUT - else - echo "โŒ Invalid provider: $PROVIDER" - echo "should-run-internal=false" >> $GITHUB_OUTPUT - echo "should-run-codecov=false" >> $GITHUB_OUTPUT + echo "๐Ÿ” Validating coverage-provider: $COVERAGE_PROVIDER" + if [[ "$COVERAGE_PROVIDER" != "internal" && "$COVERAGE_PROVIDER" != "codecov" ]]; then + echo "โŒ Invalid coverage-provider: '$COVERAGE_PROVIDER'" + echo " Valid options are: internal, codecov" + echo " Fix GO_COVERAGE_PROVIDER in .github/env/ (e.g. 10-coverage.env / 90-project.env)." exit 1 fi + echo "โœ… Coverage provider is valid: $COVERAGE_PROVIDER" # ---------------------------------------------------------------------------------- # Process Coverage and Deploy to GitHub Pages (Internal Provider) + # + # Note: Provider selection is driven by the `coverage-provider` workflow input. Test + # result validation runs as a coverage-independent job (validate-test-results in + # fortress-test-suite.yml), not inline here. # ---------------------------------------------------------------------------------- process-coverage: name: ๐Ÿ“Š Process Coverage & Deploy (Internal) - needs: check-provider - if: needs.check-provider.outputs.should-run-internal == 'true' + needs: [validate-provider] + if: inputs.coverage-provider == 'internal' runs-on: ${{ inputs.primary-runner }} timeout-minutes: 10 permissions: @@ -161,8 +155,13 @@ jobs: - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false fetch-depth: 0 # Fetch all history including tags for version display + # Note: test result validation runs as a coverage-independent job + # (validate-test-results in fortress-test-suite.yml) so failures are enforced + # even when coverage is disabled or on tag builds. This job is gated behind it. + # -------------------------------------------------------------------- # Setup Go with caching and version management # -------------------------------------------------------------------- @@ -2422,8 +2421,8 @@ jobs: # ---------------------------------------------------------------------------------- codecov-upload: name: ๐Ÿ“ˆ Upload to Codecov - needs: check-provider - if: needs.check-provider.outputs.should-run-codecov == 'true' + needs: [validate-provider] + if: inputs.coverage-provider == 'codecov' runs-on: ${{ inputs.primary-runner }} timeout-minutes: 5 permissions: @@ -2444,8 +2443,13 @@ jobs: - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false fetch-depth: 2 # Need history for codecov to detect changes + # Note: test result validation runs as a coverage-independent job + # (validate-test-results in fortress-test-suite.yml) so failures are enforced + # even when coverage is disabled or on tag builds. This job is gated behind it. + # -------------------------------------------------------------------- # Download coverage artifact # -------------------------------------------------------------------- diff --git a/.github/workflows/fortress-pre-commit.yml b/.github/workflows/fortress-pre-commit.yml index fadcba8..462de9a 100644 --- a/.github/workflows/fortress-pre-commit.yml +++ b/.github/workflows/fortress-pre-commit.yml @@ -56,6 +56,7 @@ jobs: name: ๐Ÿช Pre-commit Checks if: ${{ inputs.pre-commit-enabled == 'true' }} runs-on: ${{ inputs.primary-runner }} + timeout-minutes: 10 permissions: contents: read outputs: @@ -68,6 +69,7 @@ jobs: - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false fetch-depth: 0 # Fetch full history to enable file change detection for all commit ranges # -------------------------------------------------------------------- diff --git a/.github/workflows/fortress-release.yml b/.github/workflows/fortress-release.yml index b51de6a..6fd6822 100644 --- a/.github/workflows/fortress-release.yml +++ b/.github/workflows/fortress-release.yml @@ -54,6 +54,7 @@ jobs: release: name: ๐Ÿš€ Build and Release runs-on: ${{ inputs.primary-runner }} + timeout-minutes: 30 permissions: contents: write # Required: "magex release" creates GitHub releases and uploads assets steps: diff --git a/.github/workflows/fortress-security-scans.yml b/.github/workflows/fortress-security-scans.yml index 98369a5..d10de10 100644 --- a/.github/workflows/fortress-security-scans.yml +++ b/.github/workflows/fortress-security-scans.yml @@ -6,6 +6,11 @@ # # Maintainer: @mrz1836 # +# FAILURE HANDLING: +# - Each scan keeps `continue-on-error: true` so one failure does not mask another. +# A final `๐Ÿšจ Aggregate failures` step exits non-zero if any real failure occurred +# (Nancy rate-limit / 402 are intentionally inconclusive and do NOT fail the job). +# # ------------------------------------------------------------------------------------ name: GoFortress (Security Scans) @@ -63,37 +68,50 @@ permissions: {} jobs: # ---------------------------------------------------------------------------------- - # Ask Nancy (Dependency Checks) + # Security Scans (single consolidated job) + # + # Workflow phases (each preserved as named, conditional, continue-on-error steps): + # ๐Ÿ“ฅ Checkout (full history for gitleaks) + # ๐Ÿ”ง Parse env / Setup Go / Extract module dir / Setup MAGE-X (shared once) + # ๐Ÿ’พ Restore + install govulncheck binary cache (if govulncheck enabled) + # + # ๐Ÿ›ก๏ธ Nancy: write go list โ†’ install โ†’ run โ†’ annotations โ†’ summary โ†’ upload + # ๐Ÿ” Govulncheck: run โ†’ annotations โ†’ summary โ†’ upload + # ๐Ÿ•ต๏ธ Gitleaks: repo check โ†’ run โ†’ annotations โ†’ summary/fork notice + # + # ๐Ÿ“Š Collect + upload cache statistics (single entry for merged job) + # ๐Ÿšจ Aggregate failures (exit 1 if any scan reported a real failure) # ---------------------------------------------------------------------------------- - ask-nancy: - name: ๐Ÿ›ก๏ธ Ask Nancy (Dependency Checks) + security-scans: + name: ๐Ÿ”’ Security Scans runs-on: ${{ inputs.primary-runner }} - if: ${{ inputs.enable-nancy }} + timeout-minutes: 10 + if: ${{ inputs.enable-nancy || inputs.enable-govulncheck || inputs.enable-gitleaks }} permissions: - contents: read + contents: read # All scans + pull-requests: write # Gitleaks needs to create PR comments steps: - # -------------------------------------------------------------------- - # Checkout code (required for local actions) - # -------------------------------------------------------------------- + # ==================================================================== + # SHARED SETUP + # ==================================================================== - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 # Full history required for Gitleaks (other scans tolerate this) - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - name: ๐Ÿ”ง Parse environment variables uses: ./.github/actions/parse-env with: env-json: ${{ inputs.env-json }} - # -------------------------------------------------------------------- - # Setup Go with caching and version management - # -------------------------------------------------------------------- + # Govulncheck may need a newer Go version than the project default โ€” use it + # if set; the same Go install also covers Nancy's `go install`. - name: ๐Ÿ—๏ธ Setup Go with Cache - id: setup-ask-nancy + id: setup-security uses: ./.github/actions/setup-go-with-cache with: - go-version: ${{ inputs.go-primary-version }} + go-version: ${{ env.GOVULNCHECK_GO_VERSION || inputs.go-primary-version }} matrix-os: ${{ inputs.primary-runner }} go-primary-version: ${{ inputs.go-primary-version }} go-secondary-version: ${{ inputs.go-primary-version }} @@ -101,25 +119,65 @@ jobs: enable-multi-module: ${{ env.ENABLE_MULTI_MODULE_TESTING }} github-token: ${{ secrets.github-token }} - # -------------------------------------------------------------------- - # Extract Go module directory from GO_SUM_FILE path - # -------------------------------------------------------------------- - name: ๐Ÿ”ง Extract Go module directory uses: ./.github/actions/extract-module-dir with: go-sum-file: ${{ inputs.go-sum-file }} + - name: ๐Ÿ”ง Setup MAGE-X + if: ${{ inputs.enable-govulncheck }} + uses: ./.github/actions/setup-magex + with: + magex-version: ${{ env.MAGE_X_VERSION }} + runner-os: ${{ inputs.primary-runner }} + use-local: ${{ env.MAGE_X_USE_LOCAL }} + # -------------------------------------------------------------------- - # Write the "go" list to file for Nancy + # Govulncheck binary cache (restore + install on miss) # -------------------------------------------------------------------- - - name: ๐Ÿ“ Write go list + - name: ๐Ÿ’พ Restore govulncheck binary cache + id: govuln-cache + if: ${{ inputs.enable-govulncheck }} + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cache/govulncheck-bin + key: ${{ inputs.primary-runner }}-govulncheck-${{ env.GOVULNCHECK_VERSION }}-go${{ env.GOVULNCHECK_GO_VERSION }} + + - name: ๐Ÿ› ๏ธ Make cached govulncheck usable + if: ${{ inputs.enable-govulncheck }} + run: | + set -euo pipefail + BIN_DIR="$HOME/.cache/govulncheck-bin" + GOVULN_BIN="$BIN_DIR/govulncheck" + if [[ -f "$GOVULN_BIN" ]]; then + echo "โœ… Using cached govulncheck binary" + mkdir -p "$(go env GOPATH)/bin" + cp "$GOVULN_BIN" "$(go env GOPATH)/bin/" + fi + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: ๐Ÿ“ฅ Install govulncheck (cache miss) + if: ${{ inputs.enable-govulncheck && steps.govuln-cache.outputs.cache-hit != 'true' }} + run: | + echo "โฌ‡๏ธ Cache miss โ€“ installing govulncheck..." + echo "๐Ÿ”ง Installing govulncheck version ${{ env.GOVULNCHECK_VERSION }}..." + go install golang.org/x/vuln/cmd/govulncheck@${{ env.GOVULNCHECK_VERSION }} + mkdir -p ~/.cache/govulncheck-bin + cp "$(go env GOPATH)/bin/govulncheck" ~/.cache/govulncheck-bin/ + echo "โœ… govulncheck installed and stored in cache" + + # ==================================================================== + # NANCY (DEPENDENCY CHECKS) + # ==================================================================== + - name: ๐Ÿ“ Nancy โ€” Write go list + if: ${{ inputs.enable-nancy }} run: | echo "๐Ÿ“‹ Generating module list for security scanning..." GO_MODULE_DIR="${{ env.GO_MODULE_DIR }}" if [ "$ENABLE_MULTI_MODULE_TESTING" == "true" ]; then echo "๐Ÿ”ง Multi-module enabled - running go list from repository root" - echo "๐Ÿ“ฆ go.work will discover all Go modules" go list -json -m all > go.list elif [ -n "$GO_MODULE_DIR" ]; then echo "๐Ÿ”ง Running go list from directory: $GO_MODULE_DIR" @@ -131,20 +189,16 @@ jobs: echo "โœ… Module list generated successfully" - # -------------------------------------------------------------------- - # Install Nancy - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Install Nancy + - name: ๐Ÿ“ฅ Nancy โ€” Install + if: ${{ inputs.enable-nancy }} run: | echo "๐Ÿ“ฅ Installing nancy version ${{ env.NANCY_VERSION }}..." go install github.com/sonatype-nexus-community/nancy@${{ env.NANCY_VERSION }} echo "โœ… Nancy installed successfully" - # -------------------------------------------------------------------- - # Run Nancy to check for vulnerabilities - # -------------------------------------------------------------------- - - name: ๐Ÿ” Ask Nancy + - name: ๐Ÿ›ก๏ธ Nancy โ€” Run scan id: run-nancy + if: ${{ inputs.enable-nancy }} continue-on-error: true env: OSSI_USERNAME: ${{ secrets.ossi-username }} @@ -161,15 +215,11 @@ jobs: echo "nancy-status=success" >> $GITHUB_OUTPUT echo "โœ… Nancy scan completed - no vulnerabilities found" elif grep -qi "rate limited by OSS Index" nancy-output.log; then - # OSS Index rate-limited the scan; treat as inconclusive, NOT as a CI failure. - # Add OSSI_USERNAME and OSSI_TOKEN secrets to authenticate and lift the limit. echo "nancy-status=rate-limited" >> $GITHUB_OUTPUT echo "โš ๏ธ Nancy scan inconclusive - OSS Index rate-limited the request (not failing CI)." echo " Configure OSSI_USERNAME and OSSI_TOKEN secrets to authenticate and lift the limit." echo " Register at https://ossindex.sonatype.org/user/register" elif grep -qi "402 Payment Required" nancy-output.log; then - # OSS Index returned 402 (free-tier quota exhausted / paid plan required); - # treat as inconclusive, NOT as a CI failure. echo "nancy-status=payment-required" >> $GITHUB_OUTPUT echo "โš ๏ธ Nancy scan inconclusive - OSS Index returned 402 Payment Required (not failing CI)." echo " Configure OSSI_USERNAME and OSSI_TOKEN secrets to authenticate against your OSS Index account." @@ -179,46 +229,36 @@ jobs: echo "โŒ Nancy scan completed - vulnerabilities detected (exit code: $NANCY_EXIT_CODE)" fi - # -------------------------------------------------------------------- - # Create GitHub Annotations for failures - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations (vulnerabilities) + - name: ๐Ÿ“‹ Nancy โ€” Annotations (vulnerabilities) if: always() && steps.run-nancy.outputs.nancy-status == 'failure' run: | echo "::error title=Nancy Security Scan Failed::Vulnerabilities detected in Go dependencies - see job summary for details" - # -------------------------------------------------------------------- - # Create GitHub Annotations for rate-limited scans (warning, not error) - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations (rate-limited) + - name: ๐Ÿ“‹ Nancy โ€” Annotations (rate-limited) if: always() && steps.run-nancy.outputs.nancy-status == 'rate-limited' run: | echo "::warning title=Nancy Rate Limited::OSS Index rate-limited the scan; results inconclusive. Add OSSI_USERNAME and OSSI_TOKEN secrets to authenticate and lift the limit." - # -------------------------------------------------------------------- - # Create GitHub Annotations for 402 Payment Required (warning, not error) - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations (payment-required) + - name: ๐Ÿ“‹ Nancy โ€” Annotations (payment-required) if: always() && steps.run-nancy.outputs.nancy-status == 'payment-required' run: | echo "::warning title=Nancy Payment Required::OSS Index returned 402 Payment Required; results inconclusive. Add OSSI_USERNAME and OSSI_TOKEN secrets to authenticate against your OSS Index account." - # -------------------------------------------------------------------- - # Summary of Nancy results - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Job Summary - if: always() + - name: ๐Ÿ“Š Nancy โ€” Job Summary + if: always() && inputs.enable-nancy run: | NANCY_STATUS="${{ steps.run-nancy.outputs.nancy-status }}" - echo "## ๐Ÿ›ก๏ธ Nancy Security Scan Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Scan Details | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Tool** | Nancy Sleuth |" >> $GITHUB_STEP_SUMMARY - echo "| **Mode** | Loud mode with exclusions |" >> $GITHUB_STEP_SUMMARY - echo "| **Scope** | All Go modules |" >> $GITHUB_STEP_SUMMARY - echo "| **Version** | ${{ env.NANCY_VERSION }} |" >> $GITHUB_STEP_SUMMARY + { + echo "## ๐Ÿ›ก๏ธ Nancy Security Scan Summary" + echo "" + echo "| Scan Details | Status |" + echo "|---|---|" + echo "| **Tool** | Nancy Sleuth |" + echo "| **Mode** | Loud mode with exclusions |" + echo "| **Scope** | All Go modules |" + echo "| **Version** | ${{ env.NANCY_VERSION }} |" + } >> $GITHUB_STEP_SUMMARY if [[ "$NANCY_STATUS" == "success" ]]; then echo "| **Result** | โœ… No vulnerabilities found |" >> $GITHUB_STEP_SUMMARY @@ -230,80 +270,86 @@ jobs: echo "| **Result** | โŒ Vulnerabilities detected |" >> $GITHUB_STEP_SUMMARY fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿšซ Excluded Vulnerabilities" >> $GITHUB_STEP_SUMMARY - echo "The following vulnerabilities were excluded from the scan:" >> $GITHUB_STEP_SUMMARY - echo "${{ env.NANCY_EXCLUDES }}" >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "### ๐Ÿšซ Excluded Vulnerabilities" + echo "The following vulnerabilities were excluded from the scan:" + echo "${{ env.NANCY_EXCLUDES }}" + } >> $GITHUB_STEP_SUMMARY - # Rate-limited: explain clearly that CI was NOT failed and how to remediate if [[ "$NANCY_STATUS" == "rate-limited" ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โš ๏ธ OSS Index Rate Limit" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Sonatype's OSS Index rate-limited this request before Nancy could complete the scan." >> $GITHUB_STEP_SUMMARY - echo "**This is not a vulnerability detection** and CI has **not** been failed for this run." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**To remediate (recommended):**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "1. Register a free account at ." >> $GITHUB_STEP_SUMMARY - echo "2. Retrieve your username (email) and API token from ." >> $GITHUB_STEP_SUMMARY - echo "3. Add them as repository secrets named \`OSSI_USERNAME\` and \`OSSI_TOKEN\`." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Authenticated requests have a substantially higher rate limit and avoid this state." >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "### โš ๏ธ OSS Index Rate Limit" + echo "" + echo "Sonatype's OSS Index rate-limited this request before Nancy could complete the scan." + echo "**This is not a vulnerability detection** and CI has **not** been failed for this run." + echo "" + echo "**To remediate (recommended):**" + echo "" + echo "1. Register a free account at ." + echo "2. Retrieve your username (email) and API token from ." + echo "3. Add them as repository secrets named \`OSSI_USERNAME\` and \`OSSI_TOKEN\`." + echo "" + echo "Authenticated requests have a substantially higher rate limit and avoid this state." + } >> $GITHUB_STEP_SUMMARY if [[ -f nancy-output.log ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand Nancy output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -50 nancy-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "
" + echo "Click to expand Nancy output" + echo "" + echo '```' + head -50 nancy-output.log + echo '```' + echo "
" + } >> $GITHUB_STEP_SUMMARY fi - # Payment-required (402): explain clearly that CI was NOT failed and how to remediate elif [[ "$NANCY_STATUS" == "payment-required" ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โš ๏ธ OSS Index Payment Required (402)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Sonatype's OSS Index returned **402 Payment Required**, indicating the free-tier quota for unauthenticated requests has been exhausted." >> $GITHUB_STEP_SUMMARY - echo "**This is not a vulnerability detection** and CI has **not** been failed for this run." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**To remediate (recommended):**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "1. Register a free account at ." >> $GITHUB_STEP_SUMMARY - echo "2. Retrieve your username (email) and API token from ." >> $GITHUB_STEP_SUMMARY - echo "3. Add them as repository secrets named \`OSSI_USERNAME\` and \`OSSI_TOKEN\`." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Authenticated requests have a higher quota and avoid this state." >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "### โš ๏ธ OSS Index Payment Required (402)" + echo "" + echo "Sonatype's OSS Index returned **402 Payment Required**, indicating the free-tier quota for unauthenticated requests has been exhausted." + echo "**This is not a vulnerability detection** and CI has **not** been failed for this run." + echo "" + echo "**To remediate (recommended):**" + echo "" + echo "1. Register a free account at ." + echo "2. Retrieve your username (email) and API token from ." + echo "3. Add them as repository secrets named \`OSSI_USERNAME\` and \`OSSI_TOKEN\`." + echo "" + echo "Authenticated requests have a higher quota and avoid this state." + } >> $GITHUB_STEP_SUMMARY if [[ -f nancy-output.log ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand Nancy output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -50 nancy-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "
" + echo "Click to expand Nancy output" + echo "" + echo '```' + head -50 nancy-output.log + echo '```' + echo "
" + } >> $GITHUB_STEP_SUMMARY fi - # Real vulnerability failure: keep existing details section elif [[ "$NANCY_STATUS" == "failure" ]] && [[ -f nancy-output.log ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿšจ Vulnerability Details" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand full scan output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -200 nancy-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "### ๐Ÿšจ Vulnerability Details" + echo "" + echo "
" + echo "Click to expand full scan output" + echo "" + echo '```' + head -200 nancy-output.log + echo '```' + echo "
" + } >> $GITHUB_STEP_SUMMARY fi - # -------------------------------------------------------------------- - # Upload Nancy scan results - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload Nancy scan results - if: always() + - name: ๐Ÿ“ค Nancy โ€” Upload scan results + if: always() && inputs.enable-nancy uses: ./.github/actions/upload-artifact-resilient with: artifact-name: nancy-scan-results @@ -311,154 +357,17 @@ jobs: retention-days: "7" if-no-files-found: ignore - # -------------------------------------------------------------------- - # Collect cache statistics - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Collect cache statistics - uses: ./.github/actions/collect-cache-stats - with: - workflow-name: nancy-security - job-name: nancy-security - os: ${{ inputs.primary-runner }} - go-version: ${{ inputs.go-primary-version }} - cache-prefix: cache-stats - gomod-cache-hit: ${{ steps.setup-ask-nancy.outputs.module-cache-hit }} - gobuild-cache-hit: ${{ steps.setup-ask-nancy.outputs.build-cache-hit }} - - # -------------------------------------------------------------------- - # Upload infrastructure cache statistics - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload infrastructure cache statistics - uses: ./.github/actions/upload-statistics - with: - artifact-name: cache-stats-security-nancy - artifact-path: cache-stats-nancy-security.json - - # -------------------------------------------------------------------- - # Fail job if vulnerabilities found - # - # Only fires when nancy-status == 'failure' (real vulnerabilities). - # The 'rate-limited' status is intentionally excluded: an OSS Index rate - # limit produces an inconclusive scan, not a vulnerability finding, and - # must not red-X CI. See the run-nancy step above for the rate-limit - # detection logic. - # -------------------------------------------------------------------- - - name: ๐Ÿšจ Fail job if vulnerabilities found - if: always() && steps.run-nancy.outputs.nancy-status == 'failure' - run: | - echo "โŒ Nancy detected vulnerabilities in dependencies" - exit 1 - - # ---------------------------------------------------------------------------------- - # Govulncheck (Vulnerability Checks) - # ---------------------------------------------------------------------------------- - govulncheck: - name: ๐Ÿ” Run govulncheck (Vulnerability Scan) - runs-on: ${{ inputs.primary-runner }} - if: ${{ inputs.enable-govulncheck }} - permissions: - contents: read - steps: - # -------------------------------------------------------------------- - # Checkout code (required for local actions) - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - uses: ./.github/actions/parse-env - with: - env-json: ${{ inputs.env-json }} - - # -------------------------------------------------------------------- - # Setup Go with caching and version management - # Uses GOVULNCHECK_GO_VERSION if set, otherwise falls back to primary version - # This allows govulncheck to use a newer Go version for accurate stdlib vulnerability detection - # -------------------------------------------------------------------- - - name: ๐Ÿ—๏ธ Setup Go with Cache - id: setup-govulncheck - uses: ./.github/actions/setup-go-with-cache - with: - go-version: ${{ env.GOVULNCHECK_GO_VERSION || inputs.go-primary-version }} - matrix-os: ${{ inputs.primary-runner }} - go-primary-version: ${{ inputs.go-primary-version }} - go-secondary-version: ${{ inputs.go-primary-version }} - go-sum-file: ${{ inputs.go-sum-file }} - enable-multi-module: ${{ env.ENABLE_MULTI_MODULE_TESTING }} - github-token: ${{ secrets.github-token }} - - # -------------------------------------------------------------------- - # Extract Go module directory from GO_SUM_FILE path - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Extract Go module directory - uses: ./.github/actions/extract-module-dir - with: - go-sum-file: ${{ inputs.go-sum-file }} - - # -------------------------------------------------------------------- - # Setup MAGE-X (required for magex deps:audit command) - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Setup MAGE-X - uses: ./.github/actions/setup-magex - with: - magex-version: ${{ env.MAGE_X_VERSION }} - runner-os: ${{ inputs.primary-runner }} - use-local: ${{ env.MAGE_X_USE_LOCAL }} - - # -------------------------------------------------------------------- - # Restore (and later save) a compact cache for the govulncheck binary - # and its vulnerability DB files. - # -------------------------------------------------------------------- - - name: ๐Ÿ’พ Restore govulncheck binary cache - id: govuln-cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: | - ~/.cache/govulncheck-bin - key: ${{ inputs.primary-runner }}-govulncheck-${{ env.GOVULNCHECK_VERSION }}-go${{ env.GOVULNCHECK_GO_VERSION }} - - - name: ๐Ÿ› ๏ธ Make cached govulncheck usable - run: | - set -euo pipefail - BIN_DIR="$HOME/.cache/govulncheck-bin" - GOVULN_BIN="$BIN_DIR/govulncheck" - # If we restored a cache, copy/link it into GOPATH/bin so the binary works. - if [[ -f "$GOVULN_BIN" ]]; then - echo "โœ… Using cached govulncheck binary" - mkdir -p "$(go env GOPATH)/bin" - cp "$GOVULN_BIN" "$(go env GOPATH)/bin/" - fi - # Make sure the binary location is on PATH for *all* subsequent steps. - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - - # -------------------------------------------------------------------- - # Install govulncheck *only* when the cache was empty. - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Install govulncheck (cache miss) - if: steps.govuln-cache.outputs.cache-hit != 'true' - run: | - echo "โฌ‡๏ธ Cache miss โ€“ installing govulncheck..." - echo "๐Ÿ”ง Installing govulncheck version ${{ env.GOVULNCHECK_VERSION }}..." - go install golang.org/x/vuln/cmd/govulncheck@${{ env.GOVULNCHECK_VERSION }} - # Copy the freshly built binary back into the cache directory - mkdir -p ~/.cache/govulncheck-bin - cp "$(go env GOPATH)/bin/govulncheck" ~/.cache/govulncheck-bin/ - echo "โœ… govulncheck installed and stored in cache" - - # -------------------------------------------------------------------- - # Run govulncheck - # -------------------------------------------------------------------- - - name: ๐Ÿ” Run govulncheck + # ==================================================================== + # GOVULNCHECK (VULNERABILITY CHECKS) + # ==================================================================== + - name: ๐Ÿ” Govulncheck โ€” Run id: run-govulncheck + if: ${{ inputs.enable-govulncheck }} continue-on-error: true run: | echo "๐Ÿ” Running vulnerability analysis..." GO_MODULE_DIR="${{ env.GO_MODULE_DIR }}" - # Show CVE exclusions if configured if [ -n "${MAGE_X_CVE_EXCLUDES:-}" ]; then echo "๐Ÿšซ CVE exclusions configured: $MAGE_X_CVE_EXCLUDES" fi @@ -466,7 +375,6 @@ jobs: set +e if [ "$ENABLE_MULTI_MODULE_TESTING" == "true" ]; then echo "๐Ÿ”ง Multi-module enabled - running magex deps:audit from repository root" - echo "๐Ÿ“ฆ magex will discover all Go modules" magex deps:audit 2>&1 | tee govulncheck-output.log GOVULN_EXIT_CODE=${PIPESTATUS[0]} elif [ -n "$GO_MODULE_DIR" ]; then @@ -489,29 +397,25 @@ jobs: echo "โŒ Vulnerability scan completed - vulnerabilities detected (exit code: $GOVULN_EXIT_CODE)" fi - # -------------------------------------------------------------------- - # Create GitHub Annotations for failures - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations + - name: ๐Ÿ“‹ Govulncheck โ€” Annotations if: always() && steps.run-govulncheck.outputs.govuln-status == 'failure' run: | echo "::error title=Govulncheck Failed::Go vulnerabilities detected - see job summary for details" - # -------------------------------------------------------------------- - # Summary of govulncheck results - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Job Summary - if: always() + - name: ๐Ÿ“Š Govulncheck โ€” Job Summary + if: always() && inputs.enable-govulncheck run: | GOVULN_STATUS="${{ steps.run-govulncheck.outputs.govuln-status }}" - echo "## ๐Ÿ” govulncheck Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Analysis Details | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Tool** | govulncheck (Official Go Security Tool) |" >> $GITHUB_STEP_SUMMARY - echo "| **Installation** | $( [[ '${{ steps.govuln-cache.outputs.cache-hit }}' == 'true' ]] && echo '๐Ÿ’พ From cache' || echo '๐Ÿ“ฅ Fresh install' ) |" >> $GITHUB_STEP_SUMMARY - echo "| **Scope** | All packages in module |" >> $GITHUB_STEP_SUMMARY + { + echo "## ๐Ÿ” govulncheck Summary" + echo "" + echo "| Analysis Details | Status |" + echo "|---|---|" + echo "| **Tool** | govulncheck (Official Go Security Tool) |" + echo "| **Installation** | $( [[ '${{ steps.govuln-cache.outputs.cache-hit }}' == 'true' ]] && echo '๐Ÿ’พ From cache' || echo '๐Ÿ“ฅ Fresh install' ) |" + echo "| **Scope** | All packages in module |" + } >> $GITHUB_STEP_SUMMARY if [[ "$GOVULN_STATUS" == "success" ]]; then echo "| **Result** | โœ… No vulnerabilities detected |" >> $GITHUB_STEP_SUMMARY @@ -519,37 +423,38 @@ jobs: echo "| **Result** | โŒ Vulnerabilities detected |" >> $GITHUB_STEP_SUMMARY fi - echo "| **Version** | ${{ env.GOVULNCHECK_VERSION }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + { + echo "| **Version** | ${{ env.GOVULNCHECK_VERSION }} |" + echo "" + } >> $GITHUB_STEP_SUMMARY - # Show excluded vulnerabilities if configured if [ -n "${MAGE_X_CVE_EXCLUDES:-}" ]; then - echo "### ๐Ÿšซ Excluded Vulnerabilities" >> $GITHUB_STEP_SUMMARY - echo "The following vulnerabilities were excluded from the scan:" >> $GITHUB_STEP_SUMMARY - echo "\`${MAGE_X_CVE_EXCLUDES}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + { + echo "### ๐Ÿšซ Excluded Vulnerabilities" + echo "The following vulnerabilities were excluded from the scan:" + echo "\`${MAGE_X_CVE_EXCLUDES}\`" + echo "" + } >> $GITHUB_STEP_SUMMARY fi - # Show failure details if applicable if [[ "$GOVULN_STATUS" != "success" ]] && [[ -f govulncheck-output.log ]]; then - echo "### ๐Ÿšจ Vulnerability Details" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand full scan output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - head -200 govulncheck-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY + { + echo "### ๐Ÿšจ Vulnerability Details" + echo "" + echo "
" + echo "Click to expand full scan output" + echo "" + echo '```' + head -200 govulncheck-output.log + echo '```' + echo "
" + } >> $GITHUB_STEP_SUMMARY else echo "๐ŸŽฏ **Analysis completed successfully with no security issues found.**" >> $GITHUB_STEP_SUMMARY fi - # -------------------------------------------------------------------- - # Upload govulncheck results - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload govulncheck results - if: always() + - name: ๐Ÿ“ค Govulncheck โ€” Upload results + if: always() && inputs.enable-govulncheck uses: ./.github/actions/upload-artifact-resilient with: artifact-name: govulncheck-scan-results @@ -557,70 +462,12 @@ jobs: retention-days: "7" if-no-files-found: ignore - # -------------------------------------------------------------------- - # Collect cache statistics - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Collect cache statistics - uses: ./.github/actions/collect-cache-stats - with: - workflow-name: govulncheck-security - job-name: govulncheck-security - os: ${{ inputs.primary-runner }} - go-version: ${{ inputs.go-primary-version }} - cache-prefix: cache-stats - gomod-cache-hit: ${{ steps.setup-govulncheck.outputs.module-cache-hit }} - gobuild-cache-hit: ${{ steps.setup-govulncheck.outputs.build-cache-hit }} - - # -------------------------------------------------------------------- - # Upload infrastructure cache statistics - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload infrastructure cache statistics - uses: ./.github/actions/upload-statistics - with: - artifact-name: cache-stats-security-govulncheck - artifact-path: cache-stats-govulncheck-security.json - - # -------------------------------------------------------------------- - # Fail job if vulnerabilities found - # -------------------------------------------------------------------- - - name: ๐Ÿšจ Fail job if vulnerabilities found - if: always() && steps.run-govulncheck.outputs.govuln-status == 'failure' - run: | - echo "โŒ Govulncheck detected vulnerabilities in dependencies" - exit 1 - - # ---------------------------------------------------------------------------------- - # Gitleaks (Secret Scanning) - # ---------------------------------------------------------------------------------- - gitleaks: - name: ๐Ÿ•ต๏ธ Run Gitleaks (Secret Scan) - runs-on: ${{ inputs.primary-runner }} - if: ${{ inputs.enable-gitleaks }} - permissions: - contents: read - pull-requests: write - steps: - # -------------------------------------------------------------------- - # Checkout code (required for local actions) - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 # Fetch all history so Gitleaks can scan commits - - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - uses: ./.github/actions/parse-env - with: - env-json: ${{ inputs.env-json }} - - # -------------------------------------------------------------------- - # Check repository security conditions - # -------------------------------------------------------------------- - - name: ๐Ÿ” Check repository security conditions - id: repo-check + # ==================================================================== + # GITLEAKS (SECRET SCANNING) + # ==================================================================== + - name: ๐Ÿ” Gitleaks โ€” Check repository security conditions + id: gitleaks-repo-check + if: ${{ inputs.enable-gitleaks }} env: GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_ACTOR: ${{ github.actor }} @@ -634,8 +481,6 @@ jobs: echo "Repository: $GITHUB_REPOSITORY" echo "Head Ref: $GITHUB_HEAD_REF" - # For workflow_call, we typically trust the calling workflow from the same repo - # For pull_request events, check if head repo matches base repo if [[ "$GITHUB_EVENT_NAME" == "workflow_call" ]]; then echo "โœ… Workflow call from same repository - security scans allowed" echo "is_same_repo=true" >> $GITHUB_OUTPUT @@ -648,9 +493,9 @@ jobs: echo "is_same_repo=false" >> $GITHUB_OUTPUT fi - - name: ๐Ÿ” Run gitleaks scan + - name: ๐Ÿ•ต๏ธ Gitleaks โ€” Run scan id: run-gitleaks - if: steps.repo-check.outputs.is_same_repo == 'true' + if: ${{ inputs.enable-gitleaks && steps.gitleaks-repo-check.outputs.is_same_repo == 'true' }} continue-on-error: true # NOTE: gitleaks/gitleaks-action@v2.3.9 is the latest release and still uses Node.js 20. # This will trigger a "Node.js 20 actions are deprecated" warning until the gitleaks @@ -666,61 +511,129 @@ jobs: GITLEAKS_VERSION: ${{ env.GITLEAKS_VERSION }} GITLEAKS_CONFIG: ${{ env.GITLEAKS_CONFIG_FILE }} - # -------------------------------------------------------------------- - # Create GitHub Annotations for failures - # -------------------------------------------------------------------- - - name: ๐Ÿ“‹ Create GitHub Annotations - if: always() && steps.repo-check.outputs.is_same_repo == 'true' && steps.run-gitleaks.outcome == 'failure' + - name: ๐Ÿ“‹ Gitleaks โ€” Annotations + if: always() && inputs.enable-gitleaks && steps.gitleaks-repo-check.outputs.is_same_repo == 'true' && steps.run-gitleaks.outcome == 'failure' run: | echo "::error title=Gitleaks Secret Scan Failed::Secrets detected in repository - see job summary for details" - - name: ๐Ÿ“Š Job Summary - if: always() && steps.repo-check.outputs.is_same_repo == 'true' + - name: ๐Ÿ“Š Gitleaks โ€” Job Summary + if: always() && inputs.enable-gitleaks && steps.gitleaks-repo-check.outputs.is_same_repo == 'true' run: | GITLEAKS_OUTCOME="${{ steps.run-gitleaks.outcome }}" - echo "## ๐Ÿ•ต๏ธ Gitleaks Secret Scan Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Scan Details | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Tool** | Gitleaks |" >> $GITHUB_STEP_SUMMARY - echo "| **Version** | ${{ env.GITLEAKS_VERSION }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Scope** | All commits and files |" >> $GITHUB_STEP_SUMMARY - echo "| **Config** | $([ -n "${{ env.GITLEAKS_CONFIG_FILE }}" ] && echo "Custom: \`${{ env.GITLEAKS_CONFIG_FILE }}\`" || echo "Default (built-in rules)") |" >> $GITHUB_STEP_SUMMARY + { + echo "## ๐Ÿ•ต๏ธ Gitleaks Secret Scan Summary" + echo "" + echo "| Scan Details | Status |" + echo "|---|---|" + echo "| **Tool** | Gitleaks |" + echo "| **Version** | ${{ env.GITLEAKS_VERSION }} |" + echo "| **Scope** | All commits and files |" + echo "| **Config** | $([ -n "${{ env.GITLEAKS_CONFIG_FILE }}" ] && echo "Custom: \`${{ env.GITLEAKS_CONFIG_FILE }}\`" || echo "Default (built-in rules)") |" + } >> $GITHUB_STEP_SUMMARY if [[ "$GITLEAKS_OUTCOME" == "success" ]]; then - echo "| **Result** | โœ… No secrets detected |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐ŸŽฏ **Secret scan completed successfully.**" >> $GITHUB_STEP_SUMMARY + { + echo "| **Result** | โœ… No secrets detected |" + echo "" + echo "๐ŸŽฏ **Secret scan completed successfully.**" + } >> $GITHUB_STEP_SUMMARY else - echo "| **Result** | โŒ Secrets detected |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿšจ Action Required" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Gitleaks has detected secrets in the repository. Please review the scan output and remove any exposed secrets." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note:** The detailed findings are available in the Gitleaks-generated summary above this section." >> $GITHUB_STEP_SUMMARY + { + echo "| **Result** | โŒ Secrets detected |" + echo "" + echo "### ๐Ÿšจ Action Required" + echo "" + echo "Gitleaks has detected secrets in the repository. Please review the scan output and remove any exposed secrets." + echo "" + echo "**Note:** The detailed findings are available in the Gitleaks-generated summary above this section." + } >> $GITHUB_STEP_SUMMARY fi - - name: ๐Ÿ“Š Fork Security Notice - if: steps.repo-check.outputs.is_same_repo == 'false' + - name: ๐Ÿ“Š Gitleaks โ€” Fork Security Notice + if: ${{ inputs.enable-gitleaks && steps.gitleaks-repo-check.outputs.is_same_repo == 'false' }} run: | - echo "## ๐Ÿ•ต๏ธ Gitleaks Secret Scan Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - 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 && 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 - echo "๐Ÿ”’ **Secret scanning was skipped because this PR comes from a fork. This is a security feature to prevent secret exposure.**" >> $GITHUB_STEP_SUMMARY + { + echo "## ๐Ÿ•ต๏ธ Gitleaks Secret Scan Summary" + echo "" + echo "| ๐Ÿ”’ Security Details | โš ๏ธ Status |" + echo "|---|---|" + echo "| **Tool** | Gitleaks |" + echo "| **Fork Detected** | ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || 'N/A (not a PR event)' }} |" + echo "| **Base Repository** | ${{ github.repository }} |" + echo "| **Result** | โš ๏ธ Skipped for security (fork cannot access secrets) |" + echo "" + echo "๐Ÿ”’ **Secret scanning was skipped because this PR comes from a fork. This is a security feature to prevent secret exposure.**" + } >> $GITHUB_STEP_SUMMARY + + # ==================================================================== + # SHARED: cache statistics + final failure aggregation + # ==================================================================== + - name: ๐Ÿ“Š Collect cache statistics + if: always() + uses: ./.github/actions/collect-cache-stats + with: + workflow-name: security-scans + job-name: security-scans + os: ${{ inputs.primary-runner }} + go-version: ${{ inputs.go-primary-version }} + cache-prefix: cache-stats + gomod-cache-hit: ${{ steps.setup-security.outputs.module-cache-hit }} + gobuild-cache-hit: ${{ steps.setup-security.outputs.build-cache-hit }} + + - name: ๐Ÿ“ค Upload infrastructure cache statistics + if: always() + uses: ./.github/actions/upload-statistics + with: + artifact-name: cache-stats-security-scans + artifact-path: cache-stats-security-scans.json # -------------------------------------------------------------------- - # Fail job if secrets found + # Aggregate failures: exit non-zero if any scan reported a REAL failure. + # Preserves the per-scan isolation while still failing the parent + # workflow's `security` job as before. + # + # Intentionally NOT treated as failures: + # - Nancy rate-limited (OSS Index throttled the request โ€” inconclusive) + # - Nancy payment-required (OSS Index free-tier exhausted โ€” inconclusive) + # - Gitleaks skipped due to fork (security policy, not a finding) # -------------------------------------------------------------------- - - name: ๐Ÿšจ Fail job if secrets found - if: always() && steps.repo-check.outputs.is_same_repo == 'true' && steps.run-gitleaks.outcome == 'failure' + - name: ๐Ÿšจ Aggregate failures + if: always() + # Security: workflow_call inputs and step results are routed through + # `env:` rather than interpolated into the shell body. This prevents + # script injection (SonarCloud githubactions script-injection rule) by + # passing values as environment data, never splicing them into the script. + env: + ENABLE_NANCY: ${{ inputs.enable-nancy }} + ENABLE_GOVULNCHECK: ${{ inputs.enable-govulncheck }} + ENABLE_GITLEAKS: ${{ inputs.enable-gitleaks }} + NANCY_STATUS: ${{ steps.run-nancy.outputs.nancy-status }} + GOVULN_STATUS: ${{ steps.run-govulncheck.outputs.govuln-status }} + GITLEAKS_OUTCOME: ${{ steps.run-gitleaks.outcome }} + GITLEAKS_SAME_REPO: ${{ steps.gitleaks-repo-check.outputs.is_same_repo }} run: | - echo "โŒ Gitleaks detected secrets in the repository" - exit 1 + FAILED=0 + + if [[ "${ENABLE_NANCY}" == "true" ]] && [[ "${NANCY_STATUS}" == "failure" ]]; then + echo "โŒ Nancy detected vulnerabilities in dependencies" + FAILED=1 + fi + + if [[ "${ENABLE_GOVULNCHECK}" == "true" ]] && [[ "${GOVULN_STATUS}" == "failure" ]]; then + echo "โŒ Govulncheck detected vulnerabilities" + FAILED=1 + fi + + if [[ "${ENABLE_GITLEAKS}" == "true" ]] \ + && [[ "${GITLEAKS_SAME_REPO}" == "true" ]] \ + && [[ "${GITLEAKS_OUTCOME}" == "failure" ]]; then + echo "โŒ Gitleaks detected secrets in the repository" + FAILED=1 + fi + + if [[ $FAILED -ne 0 ]]; then + exit 1 + fi + + echo "โœ… All enabled security scans passed (or were inconclusive without finding issues)" diff --git a/.github/workflows/fortress-setup-config.yml b/.github/workflows/fortress-setup-config.yml index da9aece..9b2fd62 100644 --- a/.github/workflows/fortress-setup-config.yml +++ b/.github/workflows/fortress-setup-config.yml @@ -7,33 +7,20 @@ # # Maintainer: @mrz1836 # +# NOTE: +# - `load-env` and `test-magex` run as steps inside this workflow. The orchestrator +# (`fortress.yml`) calls this workflow directly off `paths-check`, with no +# `env-json` input โ€” env loading happens inside. +# # ------------------------------------------------------------------------------------ name: GoFortress (Setup Configuration) on: workflow_call: - inputs: - env-json: - description: "JSON string of environment variables" - required: true - type: string - primary-runner: - description: "Primary runner OS" - required: true - type: string - env-file-count: - description: "Number of env files loaded" - required: false - type: string - default: "0" - var-count: - description: "Total number of variables loaded" - required: false - type: string - default: "0" + # No inputs โ€” environment is loaded inside the job. secrets: github-token: description: "GitHub token for API access" - required: true + required: false outputs: benchmarks-enabled: description: "Whether benchmarks are enabled" @@ -164,6 +151,21 @@ on: completion-report-enabled: description: "Whether workflow completion report is enabled" value: ${{ jobs.setup-config.outputs.completion-report-enabled }} + # ---------------------------------------------------------------- + # Environment outputs + # ---------------------------------------------------------------- + env-json: + description: "JSON string of environment variables" + value: ${{ jobs.setup-config.outputs.env-json }} + env-file-count: + description: "Number of env files loaded" + value: ${{ jobs.setup-config.outputs.env-file-count }} + var-count: + description: "Total number of variables loaded" + value: ${{ jobs.setup-config.outputs.var-count }} + mage-x-version: + description: "MAGE-X version verified by setup-config" + value: ${{ jobs.setup-config.outputs.mage-x-version }} # Security: Restrict default permissions (jobs must explicitly request what they need) permissions: {} @@ -174,10 +176,17 @@ jobs: # ---------------------------------------------------------------------------------- setup-config: name: ๐Ÿ”ง Setup CI Config - runs-on: ${{ inputs.primary-runner }} + # Hard-coded runner: this job loads env from .github/env/ to discover the + # eventual `primary-runner` value, so it can't run on it. + runs-on: ubuntu-24.04 + timeout-minutes: 5 permissions: contents: read outputs: + env-json: ${{ steps.load-env.outputs.env-json }} + env-file-count: ${{ steps.load-env.outputs.env-file-count }} + var-count: ${{ steps.load-env.outputs.var-count }} + mage-x-version: ${{ steps.verify-magex.outputs.version }} benchmarks-enabled: ${{ steps.config.outputs.benchmarks-enabled }} benchmark-matrix: ${{ steps.matrix.outputs.matrix }} code-coverage-enabled: ${{ steps.config.outputs.code-coverage-enabled }} @@ -222,6 +231,21 @@ jobs: fork-security-mode: ${{ steps.fork-detection.outputs.fork-security-mode }} completion-report-enabled: ${{ steps.config.outputs.completion-report-enabled }} steps: + # ==================================================================== + # ENVIRONMENT LOAD + # ==================================================================== + - name: ๐Ÿ“ฅ Checkout (env loader) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github/env + .github/actions/load-env + + - name: ๐ŸŒ Load environment variables + id: load-env + uses: ./.github/actions/load-env + # -------------------------------------------------------------------- # Start timer to record workflow start time # -------------------------------------------------------------------- @@ -279,7 +303,7 @@ jobs: - name: ๐Ÿ”ง Parse environment variables id: parse-env env: - ENV_JSON: ${{ inputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | echo "๐Ÿ“‹ Parsing environment variables..." @@ -290,19 +314,181 @@ jobs: echo "โœ… Environment variables parsed successfully" # -------------------------------------------------------------------- - # Checkout code (sparse checkout) + # Checkout code (conditional: full when MAGE-X uses local build, otherwise + # sparse with everything needed by setup-config + the inlined test-magex + # steps below) # -------------------------------------------------------------------- + - name: ๐Ÿ“ฅ Checkout (full - MAGE-X local build) + if: env.MAGE_X_USE_LOCAL == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: ๐Ÿ“ฅ Checkout (sparse) + if: env.MAGE_X_USE_LOCAL != 'true' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false + fetch-depth: 0 # Required so the magex `metrics:mage` step can read git metadata + # Non-cone mode is required: this pattern mixes individual files + # (.mage.yaml, go.mod, go.work, go.sum, fortress.yml) with directories. + # In cone mode, `git sparse-checkout set` strict-checks each entry as a + # tree and fails with "fatal: '' is not a directory" โ€” especially + # when re-applying over the sparse state from the env-loader checkout + # earlier in this same job. + sparse-checkout-cone-mode: false sparse-checkout: | .mage.yaml go.mod go.work ${{ env.GO_SUM_FILE }} + .github/env .github/workflows/fortress.yml + .github/actions/load-env .github/actions/configure-redis .github/actions/extract-module-dir + .github/actions/setup-magex + magefiles/ + + # ==================================================================== + # MAGE-X VERIFICATION + # ==================================================================== + - name: ๐Ÿ”ง Setup MAGE-X + id: setup-magex + uses: ./.github/actions/setup-magex + with: + magex-version: ${{ env.MAGE_X_VERSION }} + runner-os: ubuntu-24.04 + use-local: ${{ env.MAGE_X_USE_LOCAL }} + + - name: ๐Ÿ” Validate MAGE-X Version + id: validate-magex-version + run: | + echo "๐Ÿ” Validating MAGE-X version..." + + ACTUAL_VERSION=$(magex --version 2>/dev/null | grep -E '^\s+Version:' | awk '{print $2}' || echo "unknown") + REQUESTED_VERSION="${{ env.MAGE_X_VERSION }}" + USE_LOCAL="${{ env.MAGE_X_USE_LOCAL }}" + + echo "๐Ÿ“‹ Build mode: $([ "$USE_LOCAL" == "true" ] && echo "local (development)" || echo "remote (release)")" + echo "๐Ÿ“‹ Requested version: $REQUESTED_VERSION" + echo "๐Ÿ“‹ Actual version: $ACTUAL_VERSION" + + if [[ "$ACTUAL_VERSION" == "unknown" ]]; then + echo "โŒ Failed: Could not determine magex version from binary" + echo "validation-result=failed" >> $GITHUB_OUTPUT + exit 1 + fi + + if [[ "$USE_LOCAL" == "true" ]]; then + if [[ "$ACTUAL_VERSION" == "dev" ]]; then + echo "โœ… Version validation passed: local build has expected 'dev' version" + echo "validation-result=success" >> $GITHUB_OUTPUT + else + echo "โš ๏ธ Warning: Local build has version '$ACTUAL_VERSION', expected 'dev'" + echo "โœ… Accepting anyway since this is a local development build" + echo "validation-result=success-with-warning" >> $GITHUB_OUTPUT + fi + else + REQUESTED_CLEAN=$(echo "$REQUESTED_VERSION" | sed 's/^v//') + ACTUAL_CLEAN=$(echo "$ACTUAL_VERSION" | sed 's/^v//') + + if [[ "$ACTUAL_CLEAN" == "$REQUESTED_CLEAN" ]]; then + echo "โœ… Version validation passed: $ACTUAL_VERSION matches $REQUESTED_VERSION" + echo "validation-result=success" >> $GITHUB_OUTPUT + else + echo "โŒ Version mismatch detected!" + echo "โŒ Requested: $REQUESTED_VERSION (clean: $REQUESTED_CLEAN)" + echo "โŒ Actual: $ACTUAL_VERSION (clean: $ACTUAL_CLEAN)" + echo "validation-result=failed" >> $GITHUB_OUTPUT + exit 1 + fi + fi + + - name: โœ… Verify MAGE-X help and required commands + id: verify-magex + run: | + echo "๐Ÿ“‹ Testing for required magex commands..." + HELP_OUTPUT=$(magex help) + echo "$HELP_OUTPUT" + + REQUIRED_COMMANDS=( + "bench" "clean" "deps:download" "deps:update" "format:fix" "lint" + "metrics:coverage" "metrics:loc" "metrics:mage" "release" + "release:validate" "test" "test:cover" "test:coverrace" "test:fuzz" + "test:race" "tidy" "update:install" "version:bump" "vet" + ) + + MATCHED_COUNT=0 + MISSING_COUNT=0 + MISSING_COMMANDS=() + + echo "" + echo "๐Ÿ” Verifying required magex commands..." + + for cmd in "${REQUIRED_COMMANDS[@]}"; do + if echo "$HELP_OUTPUT" | grep -qE "^[[:space:]]*$cmd[[:space:]]"; then + echo "โœ… Found: $cmd" + MATCHED_COUNT=$((MATCHED_COUNT + 1)) + else + echo "โŒ Missing required command: $cmd" + MISSING_COMMANDS+=("$cmd") + MISSING_COUNT=$((MISSING_COUNT + 1)) + fi + done + + # Expose the validated version so callers can read steps.verify-magex.outputs.version + MAGEX_VERSION=$(magex --version 2>/dev/null | grep -E '^\s+Version:' | awk '{print $2}' || echo "unknown") + + { + echo "matched=$MATCHED_COUNT" + echo "missing=$MISSING_COUNT" + echo "missing_commands=${MISSING_COMMANDS[*]}" + echo "version=$MAGEX_VERSION" + } >> "$GITHUB_OUTPUT" + + if [ $MISSING_COUNT -gt 0 ]; then + echo "" + echo "๐Ÿšจ Missing MAGE-X commands:" + printf ' - %s\n' "${MISSING_COMMANDS[@]}" + exit 1 + fi + + echo "" + echo "โœ… MAGE-X verification completed successfully." + + - name: ๐Ÿ“Š Collect Mage Metrics + id: mage-metrics + run: | + set -euo pipefail + echo "๐Ÿ“‹ Running magex metrics:mage..." + + METRICS_OUTPUT=$(magex metrics:mage 2>&1 || true) + echo "$METRICS_OUTPUT" + + if echo "$METRICS_OUTPUT" | grep -q "Magefiles Directory Found: โœ…"; then + echo "directory-found=true" >> $GITHUB_OUTPUT + else + echo "directory-found=false" >> $GITHUB_OUTPUT + fi + + TOTAL_FUNCTIONS=$(echo "$METRICS_OUTPUT" | grep -oE "Total functions found: [0-9]+" | grep -oE "[0-9]+" || echo "0") + echo "total-functions=$TOTAL_FUNCTIONS" >> $GITHUB_OUTPUT + + FILE_COUNT=$(echo "$METRICS_OUTPUT" | { grep -E "^\| magefiles/" || true; } | wc -l | tr -d ' ') + echo "file-count=$FILE_COUNT" >> $GITHUB_OUTPUT + + echo "" + echo "โœ… Mage metrics collected successfully." + + # Note: The MAGE-X verification SUMMARY is intentionally NOT written here. + # It's appended to $GITHUB_STEP_SUMMARY at the very end of the job (after + # the GoFortress CI Configuration summary) so the rendered order matches + # the conceptual hierarchy: GoFortress config first, supporting tool + # verification second, footer last. The verification WORK still runs here + # so the job can fail fast on MAGE-X problems. + # -------------------------------------------------------------------- # Extract Go module directory from GO_SUM_FILE path # -------------------------------------------------------------------- @@ -487,16 +673,19 @@ jobs: echo "code-coverage-enabled=${{ env.ENABLE_CODE_COVERAGE }}" >> $GITHUB_OUTPUT echo "coverage-provider=${{ env.GO_COVERAGE_PROVIDER }}" >> $GITHUB_OUTPUT - # Validate coverage provider configuration - if [ "${{ env.ENABLE_CODE_COVERAGE }}" == "true" ]; then - PROVIDER="${{ env.GO_COVERAGE_PROVIDER }}" - if [ "$PROVIDER" != "internal" ] && [ "$PROVIDER" != "codecov" ]; then - echo "โŒ Invalid GO_COVERAGE_PROVIDER: $PROVIDER" - echo " Valid options are: internal, codecov" - exit 1 - fi + # Validate coverage provider configuration ALWAYS โ€” even when coverage is + # disabled. A typo here would otherwise pass silently and, if coverage is later + # enabled, both coverage jobs (gated on internal|codecov) would just skip, + # disabling coverage without any failure. Fail fast at the first job instead. + PROVIDER="${{ env.GO_COVERAGE_PROVIDER }}" + if [ "$PROVIDER" != "internal" ] && [ "$PROVIDER" != "codecov" ]; then + echo "โŒ Invalid GO_COVERAGE_PROVIDER: $PROVIDER" + echo " Valid options are: internal, codecov" + exit 1 + fi - # Check for codecov token requirement + # Report the active provider when coverage is enabled + if [ "${{ env.ENABLE_CODE_COVERAGE }}" == "true" ]; then if [ "$PROVIDER" == "codecov" ]; then echo "โœ… Coverage provider: Codecov" else @@ -538,14 +727,14 @@ jobs: id: redis-config uses: ./.github/actions/configure-redis with: - env-json: ${{ inputs.env-json }} + env-json: ${{ steps.load-env.outputs.env-json }} # -------------------------------------------------------------------- # Build the configuration summary (Part 1: Compact Overview) # -------------------------------------------------------------------- - name: ๐Ÿ“‹ Build Configuration Summary (Part 1) id: config-summary-part1 env: - ENV_JSON: ${{ inputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | UNIQUE_GO_VERSIONS='${{ steps.versions.outputs.versions }}' MATRIX_JSON='${{ steps.matrix.outputs.matrix }}' @@ -622,8 +811,8 @@ jobs: echo "- **Workflow Start Time**: ${{ steps.timer.outputs.start-time }}" >> $GITHUB_STEP_SUMMARY # Configuration File Discovery - ENV_FILE_COUNT="${{ inputs.env-file-count }}" - VAR_COUNT="${{ inputs.var-count }}" + ENV_FILE_COUNT="${{ steps.load-env.outputs.env-file-count }}" + VAR_COUNT="${{ steps.load-env.outputs.var-count }}" echo "- **Configuration Sources**: Modular env files ($ENV_FILE_COUNT files, $VAR_COUNT variables)" >> $GITHUB_STEP_SUMMARY @@ -662,7 +851,7 @@ jobs: - name: ๐Ÿ“‹ Build Configuration Summary (Part 2) id: config-summary-part2 env: - ENV_JSON: ${{ inputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | MATRIX_JSON='${{ steps.matrix.outputs.matrix }}' MATRIX_COUNT=$(echo "$MATRIX_JSON" | jq '.include | length') @@ -710,7 +899,7 @@ jobs: - name: ๐Ÿ“‹ Build Configuration Summary (Part 3) id: config-summary-part3 env: - ENV_JSON: ${{ inputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | # Benchmark Configuration (collapsed, only if enabled) if [[ "${{ env.ENABLE_BENCHMARKS }}" == "true" ]]; then @@ -766,7 +955,7 @@ jobs: - name: ๐Ÿ“‹ Build Configuration Summary (Part 4) id: config-summary-part4 env: - ENV_JSON: ${{ inputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | # Pre-Commit System Configuration (collapsed, only if enabled) if [[ "${{ env.ENABLE_GO_PRE_COMMIT }}" == "true" ]]; then @@ -819,7 +1008,7 @@ jobs: - name: ๐Ÿ“‹ Build Configuration Summary (Part 5) id: config-summary-part5 env: - ENV_JSON: ${{ inputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | ENV_COUNT=$(echo "$ENV_JSON" | jq 'keys | length') @@ -859,7 +1048,54 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # Footer (always visible) - echo "---" >> $GITHUB_STEP_SUMMARY - echo "_๐ŸŽฏ Configuration complete at $(date -u +"%H:%M:%S UTC") โ€” GoFortress CI/CD Pipeline_" >> $GITHUB_STEP_SUMMARY + # -------------------------------------------------------------------- + # MAGE-X Verification Summary โ€” rendered AFTER GoFortress CI Configuration + # so the step summary reads top-down: GoFortress config โ†’ supporting tool + # verification โ†’ footer. The verification WORK already ran (and could have + # failed the job) much earlier; this step only renders the report. + # -------------------------------------------------------------------- + - name: ๐Ÿ“Š MAGE-X Verification Summary + if: always() + run: | + { + echo "## ๐Ÿช„ MAGE-X Verification Summary" + echo "" + echo "| Verification Details | Status |" + echo "|---|---|" + echo "| **Test** | magex help command |" + echo "| **Version** | ${{ steps.verify-magex.outputs.version }} |" + echo "| **Version Validation** | ${{ steps.validate-magex-version.outputs.validation-result == 'success' && 'โœ… Passed' || 'โŒ Failed' }} |" + echo "| **Installation** | ${{ steps.setup-magex.outputs.installation-method == 'cached' && '๐Ÿ’พ From cache' || '๐Ÿ“ฅ Fresh install' }} |" + echo "| **Purpose** | Verify MAGE-X installation and standard commands |" + echo "| **Matched Commands** | ${{ steps.verify-magex.outputs.matched }} |" + echo "| **Missing Commands** | ${{ steps.verify-magex.outputs.missing }} |" + echo "" + echo "### ๐Ÿ“ Mage Metrics" + echo "" + echo "| Metric | Value |" + echo "|---|---|" + echo "| **Magefiles Directory** | ${{ steps.mage-metrics.outputs.directory-found == 'true' && 'โœ… Found' || 'โŒ Not Found' }} |" + echo "| **Mage Files** | ${{ steps.mage-metrics.outputs.file-count }} |" + echo "| **Total Functions** | ${{ steps.mage-metrics.outputs.total-functions }} |" + echo "" + } >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.verify-magex.outputs.missing }}" != "0" ]]; then + echo "๐Ÿšจ **Missing Commands:** ${{ steps.verify-magex.outputs.missing_commands }}" >> $GITHUB_STEP_SUMMARY + else + echo "๐ŸŽฏ **MAGE-X is properly configured and functional.**" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # -------------------------------------------------------------------- + # Final footer โ€” rendered last so it always sits at the bottom of the + # setup job's summary regardless of how many sections preceded it. + # -------------------------------------------------------------------- + - name: โœ… Setup Footer + if: always() + run: | + { + echo "---" + echo "_๐ŸŽฏ Configuration complete at $(date -u +"%H:%M:%S UTC") โ€” GoFortress CI/CD Pipeline_" + } >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/fortress-test-fuzz.yml b/.github/workflows/fortress-test-fuzz.yml index 0b1d54f..58f371a 100644 --- a/.github/workflows/fortress-test-fuzz.yml +++ b/.github/workflows/fortress-test-fuzz.yml @@ -70,6 +70,8 @@ jobs: # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # -------------------------------------------------------------------- # Parse environment variables diff --git a/.github/workflows/fortress-test-magex.yml b/.github/workflows/fortress-test-magex.yml deleted file mode 100644 index a2cdaf0..0000000 --- a/.github/workflows/fortress-test-magex.yml +++ /dev/null @@ -1,278 +0,0 @@ -# ------------------------------------------------------------------------------------ -# Test MAGE-X (Reusable Workflow) (GoFortress) -# -# Purpose: Verify that MAGE-X is installed and working correctly with standard -# magex commands. This is a prerequisite for other workflows that use magex commands. -# -# Maintainer: @mrz1836 -# -# ------------------------------------------------------------------------------------ - -name: GoFortress (Test MAGE-X) - -on: - workflow_call: - inputs: - env-json: - description: "JSON string of environment variables" - required: true - type: string - primary-runner: - description: "Primary runner OS" - required: true - type: string - -# Security: Restrict default permissions (jobs must explicitly request what they need) -permissions: {} - -jobs: - # ---------------------------------------------------------------------------------- - # Test MAGE-X (Installation and Command Verification) - # ---------------------------------------------------------------------------------- - test-magex: - name: ๐Ÿช„ Verify & Test MAGE-X - runs-on: ${{ inputs.primary-runner }} - permissions: - contents: read - steps: - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - env: - ENV_JSON: ${{ inputs.env-json }} - run: | - echo "๐Ÿ“‹ Setting environment variables..." - echo "$ENV_JSON" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' | while IFS='=' read -r key value; do - echo "$key=$value" >> $GITHUB_ENV - done - - # -------------------------------------------------------------------- - # Checkout code (conditional: full or sparse based on MAGE_X_USE_LOCAL) - # -------------------------------------------------------------------- - # Full checkout when using local build (needs cmd/magex directory) - - name: ๐Ÿ“ฅ Checkout (full - local build) - if: env.MAGE_X_USE_LOCAL == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - # Sparse checkout when using remote build (optimization) - - name: ๐Ÿ“ฅ Checkout (sparse - remote build) - if: env.MAGE_X_USE_LOCAL == 'false' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 # Required for sparse checkout - sparse-checkout: | - .github/env - .github/actions/setup-magex - .mage.yaml - go.mod - ${{ env.GO_SUM_FILE }} - magefiles/ - - # -------------------------------------------------------------------- - # Setup MAGE-X using the reusable composite action - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Setup MAGE-X - id: setup-magex - uses: ./.github/actions/setup-magex - with: - magex-version: ${{ env.MAGE_X_VERSION }} - runner-os: ${{ inputs.primary-runner }} - use-local: ${{ env.MAGE_X_USE_LOCAL }} - - # -------------------------------------------------------------------- - # Validate MAGE-X version matches requested version - # -------------------------------------------------------------------- - - name: ๐Ÿ” Validate MAGE-X Version - id: validate-version - run: | - echo "๐Ÿ” Validating MAGE-X version..." - - # Get the actual version from the binary - ACTUAL_VERSION=$(magex --version 2>/dev/null | grep -E '^\s+Version:' | awk '{print $2}' || echo "unknown") - REQUESTED_VERSION="${{ env.MAGE_X_VERSION }}" - USE_LOCAL="${{ env.MAGE_X_USE_LOCAL }}" - - echo "๐Ÿ“‹ Build mode: $([ "$USE_LOCAL" == "true" ] && echo "local (development)" || echo "remote (release)")" - echo "๐Ÿ“‹ Requested version: $REQUESTED_VERSION" - echo "๐Ÿ“‹ Actual version: $ACTUAL_VERSION" - - if [[ "$ACTUAL_VERSION" == "unknown" ]]; then - echo "โŒ Failed: Could not determine magex version from binary" - echo "โŒ This indicates a problem with version detection or binary corruption" - echo "validation-result=failed" >> $GITHUB_OUTPUT - exit 1 - fi - - # Local builds should have "dev" version - if [[ "$USE_LOCAL" == "true" ]]; then - if [[ "$ACTUAL_VERSION" == "dev" ]]; then - echo "โœ… Version validation passed: local build has expected 'dev' version" - echo "validation-result=success" >> $GITHUB_OUTPUT - else - echo "โš ๏ธ Warning: Local build has version '$ACTUAL_VERSION', expected 'dev'" - echo "โš ๏ธ This might indicate the version file hasn't been updated" - echo "โœ… Accepting anyway since this is a local development build" - echo "validation-result=success-with-warning" >> $GITHUB_OUTPUT - fi - else - # Remote builds should match the requested version - REQUESTED_CLEAN=$(echo "$REQUESTED_VERSION" | sed 's/^v//') - ACTUAL_CLEAN=$(echo "$ACTUAL_VERSION" | sed 's/^v//') - - if [[ "$ACTUAL_CLEAN" == "$REQUESTED_CLEAN" ]]; then - echo "โœ… Version validation passed: $ACTUAL_VERSION matches $REQUESTED_VERSION" - echo "validation-result=success" >> $GITHUB_OUTPUT - else - echo "โŒ Version mismatch detected!" - echo "โŒ Requested: $REQUESTED_VERSION (clean: $REQUESTED_CLEAN)" - echo "โŒ Actual: $ACTUAL_VERSION (clean: $ACTUAL_CLEAN)" - echo "โŒ This indicates a cache corruption or installation failure" - echo "validation-result=failed" >> $GITHUB_OUTPUT - exit 1 - fi - fi - - # -------------------------------------------------------------------- - # Verify MAGE-X installation and required commands - # -------------------------------------------------------------------- - - name: โœ… Verify magex help and required commands - id: verify-magex - run: | - echo "๐Ÿ“‹ Testing for required magex commands..." - - # Capture help output - HELP_OUTPUT=$(magex help) - - echo "$HELP_OUTPUT" - - # List of required magex commands - REQUIRED_COMMANDS=( - "bench" - "clean" - "deps:download" - "deps:update" - "format:fix" - "lint" - "metrics:coverage" - "metrics:loc" - "metrics:mage" - "release" - "release:validate" - "test" - "test:cover" - "test:coverrace" - "test:fuzz" - "test:race" - "tidy" - "update:install" - "version:bump" - "vet" - ) - - MATCHED_COUNT=0 - MISSING_COUNT=0 - MISSING_COMMANDS=() - - echo "" - echo "๐Ÿ” Verifying required magex commands..." - - for cmd in "${REQUIRED_COMMANDS[@]}"; do - if echo "$HELP_OUTPUT" | grep -qE "^[[:space:]]*$cmd[[:space:]]"; then - echo "โœ… Found: $cmd" - MATCHED_COUNT=$((MATCHED_COUNT + 1)) - else - echo "โŒ Missing required command: $cmd" - MISSING_COMMANDS+=("$cmd") - MISSING_COUNT=$((MISSING_COUNT + 1)) - fi - done - - echo "" - echo "โœ… Matched: $MATCHED_COUNT" - echo "โŒ Missing: $MISSING_COUNT" - - echo "matched=$MATCHED_COUNT" >> "$GITHUB_OUTPUT" - echo "missing=$MISSING_COUNT" >> "$GITHUB_OUTPUT" - echo "missing_commands=${MISSING_COMMANDS[*]}" >> "$GITHUB_OUTPUT" - - # Fail if anything is missing - if [ $MISSING_COUNT -gt 0 ]; then - echo "" - echo "๐Ÿšจ Missing MAGE-X commands:" - printf ' - %s\n' "${MISSING_COMMANDS[@]}" - exit 1 - fi - - echo "" - echo "โœ… MAGE-X verification completed successfully." - - # -------------------------------------------------------------------- - # Collect Mage Metrics - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Collect Mage Metrics - id: mage-metrics - run: | - set -euo pipefail - echo "๐Ÿ“‹ Running magex metrics:mage..." - - # Run magex metrics:mage and capture output - METRICS_OUTPUT=$(magex metrics:mage 2>&1 || true) - echo "$METRICS_OUTPUT" - - # Parse directory found status - if echo "$METRICS_OUTPUT" | grep -q "Magefiles Directory Found: โœ…"; then - echo "directory-found=true" >> $GITHUB_OUTPUT - else - echo "directory-found=false" >> $GITHUB_OUTPUT - fi - - # Parse total functions count - TOTAL_FUNCTIONS=$(echo "$METRICS_OUTPUT" | grep -oE "Total functions found: [0-9]+" | grep -oE "[0-9]+" || echo "0") - echo "total-functions=$TOTAL_FUNCTIONS" >> $GITHUB_OUTPUT - - # Count number of mage files (lines in table excluding header) - FILE_COUNT=$(echo "$METRICS_OUTPUT" | { grep -E "^\| magefiles/" || true; } | wc -l | tr -d ' ') - echo "file-count=$FILE_COUNT" >> $GITHUB_OUTPUT - - echo "" - echo "โœ… Mage metrics collected successfully." - - # -------------------------------------------------------------------- - # Summary of MAGE-X verification - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Job Summary - run: | - # Get magex version - MAGEX_VERSION=$(magex --version 2>/dev/null | grep -E '^\s+Version:' | awk '{print $2}' || echo "unknown") - - echo "## ๐Ÿช„ MAGE-X Verification Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Verification Details | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Test** | magex help command |" >> $GITHUB_STEP_SUMMARY - echo "| **Version** | $MAGEX_VERSION |" >> $GITHUB_STEP_SUMMARY - echo "| **Version Validation** | ${{ steps.validate-version.outputs.validation-result == 'success' && 'โœ… Passed' || 'โŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Installation** | ${{ steps.setup-magex.outputs.installation-method == 'cached' && '๐Ÿ’พ From cache' || '๐Ÿ“ฅ Fresh install' }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Purpose** | Verify MAGE-X installation and standard commands |" >> $GITHUB_STEP_SUMMARY - echo "| **Matched Commands** | ${{ steps.verify-magex.outputs.matched }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Missing Commands** | ${{ steps.verify-magex.outputs.missing }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Mage Metrics Section - echo "### ๐Ÿ“ Mage Metrics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| **Magefiles Directory** | ${{ steps.mage-metrics.outputs.directory-found == 'true' && 'โœ… Found' || 'โŒ Not Found' }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Mage Files** | ${{ steps.mage-metrics.outputs.file-count }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Total Functions** | ${{ steps.mage-metrics.outputs.total-functions }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ steps.verify-magex.outputs.missing }}" != "0" ]]; then - echo "๐Ÿšจ **Missing Commands:** ${{ steps.verify-magex.outputs.missing_commands }}" >> $GITHUB_STEP_SUMMARY - else - echo "๐ŸŽฏ **MAGE-X is properly configured and functional.**" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/fortress-test-matrix.yml b/.github/workflows/fortress-test-matrix.yml index 820e99c..1a9a251 100644 --- a/.github/workflows/fortress-test-matrix.yml +++ b/.github/workflows/fortress-test-matrix.yml @@ -133,6 +133,8 @@ jobs: # -------------------------------------------------------------------- - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # -------------------------------------------------------------------- # Parse environment variables diff --git a/.github/workflows/fortress-test-suite.yml b/.github/workflows/fortress-test-suite.yml index 45e302e..69ed944 100644 --- a/.github/workflows/fortress-test-suite.yml +++ b/.github/workflows/fortress-test-suite.yml @@ -7,8 +7,8 @@ # This workflow coordinates sub-workflows for improved maintainability: # - fortress-test-matrix.yml: Multi-platform test execution # - fortress-test-fuzz.yml: Fuzz testing execution -# - fortress-test-validation.yml: Test result validation -# - fortress-coverage.yml: Coverage processing (existing) +# - fortress-coverage.yml: Coverage processing (coverage only โ€” validation is +# handled by the validate-test-results job in this workflow) # # Maintainer: @mrz1836 # @@ -168,23 +168,48 @@ jobs: github-token: ${{ secrets.github-token }} # ---------------------------------------------------------------------------------- - # Test Results Validation (Aggregate all test results) + # Test Results Validation (coverage-independent aggregate gate) + # + # This job MUST be independent of coverage so test failures are always enforced โ€” even + # when coverage is disabled, on tag builds, or when the coverage provider is + # misconfigured. The test matrix/fuzz jobs run with `continue-on-error: true` (so every + # shard finishes and reports its summary) and never re-fail themselves; this aggregate + # job is the single thing that turns "a test failed" into "the workflow failed". It + # reuses the .github/actions/validate-test-results composite action, wired here as + # its own always-running job. # ---------------------------------------------------------------------------------- validate-test-results: name: ๐Ÿ” Validate Test Results needs: [execute-test-matrix, execute-fuzz-tests] - if: always() && inputs.go-tests-enabled == 'true' # Always run to validate results even if tests failed + # Always run to validate results even if tests "continued on error" above. + if: always() && inputs.go-tests-enabled == 'true' permissions: - contents: read - actions: read - uses: ./.github/workflows/fortress-test-validation.yml - with: - env-json: ${{ inputs.env-json }} - primary-runner: ${{ inputs.primary-runner }} - fuzz-testing-enabled: ${{ inputs.fuzz-testing-enabled }} + contents: read # Read repository content for validation + actions: read # Required: download CI result artifacts from the test jobs + runs-on: ${{ inputs.primary-runner }} + timeout-minutes: 10 + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: ๐Ÿ”ง Parse environment variables + uses: ./.github/actions/parse-env + with: + env-json: ${{ inputs.env-json }} + + - name: ๐Ÿ” Validate test results + uses: ./.github/actions/validate-test-results + with: + fuzz-testing-enabled: ${{ inputs.fuzz-testing-enabled }} # ---------------------------------------------------------------------------------- - # Coverage Processing (Existing workflow integration) + # Coverage Processing + # + # Gated behind validate-test-results: without `always()`, this job is skipped when + # validation fails, so coverage is never processed for a failing test run. Coverage + # remains optional (code-coverage-enabled) and is skipped on tag builds. # ---------------------------------------------------------------------------------- process-coverage: name: ๐Ÿ“Š Process Coverage @@ -207,6 +232,7 @@ jobs: event-name: ${{ github.event_name }} pr-number: ${{ github.event.pull_request.number }} go-sum-file: ${{ inputs.go-sum-file }} + coverage-provider: ${{ inputs.coverage-provider }} secrets: github-token: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/fortress-test-validation.yml b/.github/workflows/fortress-test-validation.yml deleted file mode 100644 index 8112a6d..0000000 --- a/.github/workflows/fortress-test-validation.yml +++ /dev/null @@ -1,420 +0,0 @@ -# ------------------------------------------------------------------------------------ -# Test Results Validation (Reusable Workflow) (GoFortress) -# -# Purpose: Validate and aggregate test results from all test workflows using -# native CI mode output (.mage-x/ci-results.jsonl). -# -# This workflow handles: -# - Downloading CI results artifacts from test workflows -# - Validating test exit codes and failure counts -# - Aggregating failures across matrix jobs -# - Creating comprehensive validation reports -# -# CI Mode Integration: magex CI mode produces .mage-x/ci-results.jsonl which -# contains structured failure data with built-in deduplication. -# -# Maintainer: @mrz1836 -# -# ------------------------------------------------------------------------------------ - -name: GoFortress (Test Validation) - -on: - workflow_call: - inputs: - env-json: - description: "JSON string of environment variables" - required: true - type: string - primary-runner: - description: "Primary runner OS" - required: true - type: string - fuzz-testing-enabled: - description: "Whether fuzz testing is enabled" - required: true - type: string - -# Security: Restrict default permissions (jobs must explicitly request what they need) -permissions: {} - -jobs: - # ---------------------------------------------------------------------------------- - # Validate Test Results - # ---------------------------------------------------------------------------------- - validate-test-results: - name: ๐Ÿ” Validate Test Results - if: always() # Always run to check results even if jobs continued on error - permissions: - contents: read # Read repository content for validation - actions: read # Read workflow artifacts for download - runs-on: ${{ inputs.primary-runner }} - - steps: - # -------------------------------------------------------------------- - # Checkout code (required for local actions) - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # -------------------------------------------------------------------- - # Parse environment variables - # -------------------------------------------------------------------- - - name: ๐Ÿ”ง Parse environment variables - uses: ./.github/actions/parse-env - with: - env-json: ${{ inputs.env-json }} - - # -------------------------------------------------------------------- - # Download CI results from test matrix - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Download CI results - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "ci-results-*" - path: ci-results/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - # -------------------------------------------------------------------- - # Download fuzz test results if enabled - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Download fuzz test results - if: inputs.fuzz-testing-enabled == 'true' - uses: ./.github/actions/download-artifact-resilient - with: - pattern: "test-results-fuzz-*" - path: ci-results/ - merge-multiple: true - max-retries: ${{ env.ARTIFACT_DOWNLOAD_RETRIES }} - retry-delay: ${{ env.ARTIFACT_DOWNLOAD_RETRY_DELAY }} - timeout: ${{ env.ARTIFACT_DOWNLOAD_TIMEOUT }} - continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - - # -------------------------------------------------------------------- - # Validate test results from CI mode JSONL output - # -------------------------------------------------------------------- - - name: ๐Ÿ” Validate test results - run: | - echo "๐Ÿ” Validating test results from CI mode output..." - source .github/scripts/parse-test-label.sh - - VALIDATION_FAILED=false - TOTAL_FAILURES=0 - TOTAL_UNIQUE=0 - TOTAL_TESTS=0 - TOTAL_SKIPPED=0 - - # Find all CI results files - echo "๐Ÿ“‹ Looking for CI results files..." - find ci-results/ -name "ci-results.jsonl" -o -name "*.jsonl" 2>/dev/null | head -20 - - # Process each CI results file - # Note: Using find for recursive directory traversal to locate all matching files - if find ci-results/ -name "*.jsonl" 2>/dev/null | grep -q .; then - echo "โœ… Found CI results JSONL files" - - while IFS= read -r -d '' jsonl_file; do - # Extract artifact directory name from JSONL file path - # Supported directory structures: - # 1. Expected: ci-results/ARTIFACT_NAME/.mage-x/ci-results.jsonl - # โ†’ Use grandparent (skip .mage-x) to get ARTIFACT_NAME - # 2. Fallback: ci-results/ARTIFACT_NAME/ci-results.jsonl - # โ†’ Use parent directory as ARTIFACT_NAME - ARTIFACT_DIR=$(dirname "$(dirname "$jsonl_file")" | xargs basename) - JSONL_NAME=$(basename "$jsonl_file") - - # Detect which structure we have by checking parent directory - PARENT_DIR=$(basename "$(dirname "$jsonl_file")") - if [[ "$PARENT_DIR" != ".mage-x" ]]; then - echo " Warning: Unexpected artifact structure for: $jsonl_file" - echo " Expected: ci-results/ARTIFACT_NAME/.mage-x/ci-results.jsonl" - # Fallback: parent is the artifact dir (not grandparent) - ARTIFACT_DIR=$(basename "$(dirname "$jsonl_file")") - fi - - TEST_LABEL=$(parse_test_label "$ARTIFACT_DIR" "$JSONL_NAME") - - echo "" - echo "๐Ÿ“„ Processing: $TEST_LABEL" - - # Extract summary line (type: summary) - SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") - - if [[ -n "$SUMMARY" ]]; then - # Parse summary data - STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') - PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') - FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') - SKIPPED=$(echo "$SUMMARY" | jq -r '.summary.skipped // 0') - UNIQUE=$(echo "$SUMMARY" | jq -r '.summary.unique_total // 0') - TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') - DURATION=$(echo "$SUMMARY" | jq -r '.summary.duration // "unknown"') - - echo " โ€ข Status: $STATUS" - echo " โ€ข Passed: $PASSED" - echo " โ€ข Failed: $FAILED" - echo " โ€ข Skipped: $SKIPPED" - echo " โ€ข Unique Tests: $UNIQUE" - echo " โ€ข Test Runs: $TOTAL" - echo " โ€ข Duration: $DURATION" - - TOTAL_UNIQUE=$((TOTAL_UNIQUE + UNIQUE)) - TOTAL_TESTS=$((TOTAL_TESTS + TOTAL)) - TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED)) - - if [[ "$STATUS" == "failed" ]] || [[ "$FAILED" -gt 0 ]]; then - VALIDATION_FAILED=true - TOTAL_FAILURES=$((TOTAL_FAILURES + FAILED)) - - # Extract failure details - echo "" - echo " ๐Ÿšจ Failures in this file:" - grep '"type":"failure"' "$jsonl_file" 2>/dev/null | while read -r line; do - TEST=$(echo "$line" | jq -r '.failure.test // "unknown"') - PKG=$(echo "$line" | jq -r '.failure.package // "unknown"' | sed 's|.*/||') - FILE=$(echo "$line" | jq -r '.failure.file // ""') - LINE_NUM=$(echo "$line" | jq -r '.failure.line // ""') - FAIL_TYPE=$(echo "$line" | jq -r '.failure.type // "test"') - ERROR_MSG=$(echo "$line" | jq -r '.failure.error // ""') - - # Show test name with type and location - if [[ -n "$FILE" && -n "$LINE_NUM" && "$LINE_NUM" != "0" ]]; then - echo " โŒ [$FAIL_TYPE] $TEST ($PKG) at $FILE:$LINE_NUM" - else - echo " โŒ [$FAIL_TYPE] $TEST ($PKG)" - fi - - # Show error message if available (truncated for readability) - if [[ -n "$ERROR_MSG" && "$ERROR_MSG" != "null" ]]; then - echo " โ†’ ${ERROR_MSG:0:200}" - fi - done | head -30 - fi - else - echo " โš ๏ธ No summary found in JSONL file" - - # Try to count failures directly - FAILURE_COUNT=$(grep -c '"type":"failure"' "$jsonl_file" 2>/dev/null || echo "0") - if [[ "$FAILURE_COUNT" -gt 0 ]]; then - echo " โ€ข Found $FAILURE_COUNT failure entries" - VALIDATION_FAILED=true - TOTAL_FAILURES=$((TOTAL_FAILURES + FAILURE_COUNT)) - fi - fi - done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) - else - echo "โš ๏ธ No JSONL files found - checking for test-output.log files..." - - # Fallback: check test-output.log files for exit codes - while IFS= read -r -d '' log_file; do - echo "๐Ÿ“„ Checking: $log_file" - - # Look for FAIL indicators - if grep -q "^FAIL" "$log_file" 2>/dev/null || grep -q "--- FAIL:" "$log_file" 2>/dev/null; then - echo " โŒ Found test failures in log file" - VALIDATION_FAILED=true - FAIL_COUNT=$(grep -c "^--- FAIL:" "$log_file" 2>/dev/null || echo "1") - TOTAL_FAILURES=$((TOTAL_FAILURES + FAIL_COUNT)) - fi - done < <(find ci-results/ -name "test-output.log" -print0 2>/dev/null) - fi - - # Final validation result - echo "" - echo "๐Ÿ Validation Summary:" - echo " โ€ข Unique Tests: $TOTAL_UNIQUE" - echo " โ€ข Test Runs: $TOTAL_TESTS" - echo " โ€ข Total Failures: $TOTAL_FAILURES" - echo " โ€ข Total Skipped: $TOTAL_SKIPPED" - echo " โ€ข Validation Status: $(if [[ "$VALIDATION_FAILED" == "true" ]]; then echo "FAILED"; else echo "PASSED"; fi)" - - if [[ "$VALIDATION_FAILED" == "true" ]]; then - echo "" - echo "โŒ Test validation failed - $TOTAL_FAILURES failure(s) detected" - echo "::error title=Test Validation Failed::$TOTAL_FAILURES test failure(s) detected. Check the CI results above for details." - exit 1 - else - echo "" - echo "โœ… All tests passed validation" - fi - - # -------------------------------------------------------------------- - # Create validation summary for GitHub UI - # -------------------------------------------------------------------- - - name: ๐Ÿ“Š Create validation summary - if: always() - run: | - source .github/scripts/parse-test-label.sh - - echo "## ๐Ÿ” Test Validation Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Count artifacts - MATRIX_JOBS=$(find ci-results/ -name "*.jsonl" 2>/dev/null | wc -l || echo "0") - echo "- **Matrix Jobs Validated**: $MATRIX_JOBS" >> $GITHUB_STEP_SUMMARY - - # Aggregate results from JSONL files - TOTAL_PASSED=0 - TOTAL_FAILED=0 - TOTAL_SKIPPED=0 - TOTAL_UNIQUE=0 - TOTAL_TESTS=0 - - while IFS= read -r -d '' jsonl_file; do - SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") - if [[ -n "$SUMMARY" ]]; then - PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') - FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') - SKIPPED=$(echo "$SUMMARY" | jq -r '.summary.skipped // 0') - UNIQUE=$(echo "$SUMMARY" | jq -r '.summary.unique_total // 0') - TOTAL=$(echo "$SUMMARY" | jq -r '.summary.total // 0') - TOTAL_PASSED=$((TOTAL_PASSED + PASSED)) - TOTAL_FAILED=$((TOTAL_FAILED + FAILED)) - TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED)) - TOTAL_UNIQUE=$((TOTAL_UNIQUE + UNIQUE)) - TOTAL_TESTS=$((TOTAL_TESTS + TOTAL)) - fi - done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) - - echo "- **Unique Tests**: $TOTAL_UNIQUE" >> $GITHUB_STEP_SUMMARY - echo "- **Test Runs**: $TOTAL_TESTS" >> $GITHUB_STEP_SUMMARY - echo "- **Passed**: $TOTAL_PASSED" >> $GITHUB_STEP_SUMMARY - echo "- **Failed**: $TOTAL_FAILED" >> $GITHUB_STEP_SUMMARY - echo "- **Skipped**: $TOTAL_SKIPPED" >> $GITHUB_STEP_SUMMARY - echo "- **Validation Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ inputs.fuzz-testing-enabled }}" == "true" ]]; then - echo "- **Fuzz Testing**: Enabled" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - - # Show per-job breakdown - if [[ $MATRIX_JOBS -gt 0 ]]; then - echo "### Test Matrix Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - while IFS= read -r -d '' jsonl_file; do - # Extract artifact directory name from JSONL file path - # Supported directory structures: - # 1. Expected: ci-results/ARTIFACT_NAME/.mage-x/ci-results.jsonl - # โ†’ Use grandparent (skip .mage-x) to get ARTIFACT_NAME - # 2. Fallback: ci-results/ARTIFACT_NAME/ci-results.jsonl - # โ†’ Use parent directory as ARTIFACT_NAME - ARTIFACT_DIR=$(dirname "$(dirname "$jsonl_file")" | xargs basename) - JSONL_NAME=$(basename "$jsonl_file") - - # Detect which structure we have by checking parent directory - PARENT_DIR=$(basename "$(dirname "$jsonl_file")") - if [[ "$PARENT_DIR" != ".mage-x" ]]; then - # Fallback: parent is the artifact dir (not grandparent) - ARTIFACT_DIR=$(basename "$(dirname "$jsonl_file")") - fi - - TEST_LABEL=$(parse_test_label "$ARTIFACT_DIR" "$JSONL_NAME") - - SUMMARY=$(grep '"type":"summary"' "$jsonl_file" 2>/dev/null | head -1 || echo "") - - if [[ -n "$SUMMARY" ]]; then - STATUS=$(echo "$SUMMARY" | jq -r '.summary.status // "unknown"') - PASSED=$(echo "$SUMMARY" | jq -r '.summary.passed // 0') - FAILED=$(echo "$SUMMARY" | jq -r '.summary.failed // 0') - - if [[ "$STATUS" == "passed" ]]; then - echo "- โœ… **$TEST_LABEL**: $PASSED tests passed" >> $GITHUB_STEP_SUMMARY - else - echo "- โŒ **$TEST_LABEL**: $FAILED failures" >> $GITHUB_STEP_SUMMARY - fi - fi - done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) - fi - - # Add detailed failure section if there are failures - if [[ $TOTAL_FAILED -gt 0 ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿšจ Failure Details" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "_Expand each failure to see full output and stack traces_" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - FAILURE_COUNT=0 - while IFS= read -r -d '' jsonl_file; do - while read -r line; do - # Limit total failures shown - FAILURE_COUNT=$((FAILURE_COUNT + 1)) - if [[ $FAILURE_COUNT -gt 20 ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "_... additional failures truncated_" >> $GITHUB_STEP_SUMMARY - break 2 - fi - - TEST=$(echo "$line" | jq -r '.failure.test // "unknown"') - PKG=$(echo "$line" | jq -r '.failure.package // "unknown"' | sed 's|.*/||') - FAIL_TYPE=$(echo "$line" | jq -r '.failure.type // "test"') - ERROR_MSG=$(echo "$line" | jq -r '.failure.error // ""') - OUTPUT=$(echo "$line" | jq -r '.failure.output // ""') - STACK=$(echo "$line" | jq -r '.failure.stack // ""') - - echo "
" >> $GITHUB_STEP_SUMMARY - echo "โŒ $TEST ($PKG) - $FAIL_TYPE" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [[ -n "$ERROR_MSG" && "$ERROR_MSG" != "null" ]]; then - echo "**Error:** \`$ERROR_MSG\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - if [[ -n "$OUTPUT" && "$OUTPUT" != "null" && "$OUTPUT" != "" ]]; then - echo "**Output:**" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - # Truncate output to avoid massive summaries - echo "${OUTPUT:0:2000}" >> $GITHUB_STEP_SUMMARY - if [[ ${#OUTPUT} -gt 2000 ]]; then - echo "... (truncated)" >> $GITHUB_STEP_SUMMARY - fi - echo '```' >> $GITHUB_STEP_SUMMARY - fi - - if [[ -n "$STACK" && "$STACK" != "null" && "$STACK" != "" ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Stack Trace:**" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${STACK:0:1500}" >> $GITHUB_STEP_SUMMARY - if [[ ${#STACK} -gt 1500 ]]; then - echo "... (truncated)" >> $GITHUB_STEP_SUMMARY - fi - echo '```' >> $GITHUB_STEP_SUMMARY - fi - - # For fuzz tests, show fuzz-specific info - FUZZ_INFO=$(echo "$line" | jq -r '.failure.fuzz_info // null') - if [[ "$FUZZ_INFO" != "null" && -n "$FUZZ_INFO" ]]; then - CORPUS=$(echo "$FUZZ_INFO" | jq -r '.corpus_path // ""') - if [[ -n "$CORPUS" && "$CORPUS" != "null" ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Fuzz Corpus:** \`$CORPUS\`" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - done < <(grep '"type":"failure"' "$jsonl_file" 2>/dev/null) - done < <(find ci-results/ -name "*.jsonl" -print0 2>/dev/null) - fi - - # -------------------------------------------------------------------- - # Upload validation artifacts - # -------------------------------------------------------------------- - - name: ๐Ÿ“ค Upload validation summary - if: always() - uses: ./.github/actions/upload-artifact-resilient - with: - artifact-name: validation-summary - artifact-path: ci-results/ - retention-days: "7" - if-no-files-found: ignore diff --git a/.github/workflows/fortress-warm-cache.yml b/.github/workflows/fortress-warm-cache.yml index 5e253af..d00477c 100644 --- a/.github/workflows/fortress-warm-cache.yml +++ b/.github/workflows/fortress-warm-cache.yml @@ -68,6 +68,7 @@ jobs: fail-fast: true matrix: ${{ fromJSON(inputs.warm-cache-matrix) }} runs-on: ${{ matrix.os }} + timeout-minutes: 15 steps: # -------------------------------------------------------------------- # Parse environment variables diff --git a/.github/workflows/fortress.yml b/.github/workflows/fortress.yml index c2535d0..3c6401e 100644 --- a/.github/workflows/fortress.yml +++ b/.github/workflows/fortress.yml @@ -1,7 +1,7 @@ # ------------------------------------------------------------------------------------ # ๐Ÿฐ GoFortress - Enterprise-grade CI/CD fortress for Go applications # -# Version: 1.7.3 | Released: 2026-04-28 +# Version: 1.8.1 | Released: 2026-05-29 # # Built Strong. Tested Harder. # @@ -31,7 +31,7 @@ # and conditionally skipping jobs that require repository secrets. Jobs are categorized: # # FORK-SAFE (Always run - secrets optional for private module auth): -# โœ… setup, test-magex, warm-cache, code-quality, pre-commit, benchmarks, status-check +# โœ… setup, warm-cache, code-quality, pre-commit, benchmarks, status-check # Note: These jobs receive github-token for private Go module authentication (GOPRIVATE). # On fork PRs, private module auth is skipped but jobs still run for public dependencies. # @@ -73,76 +73,92 @@ concurrency: jobs: # ---------------------------------------------------------------------------------- - # Load Environment Variables and Setup Configuration + # Paths Filter (short-circuit for docs-only PRs) + # + # Decides whether this run touches only documentation/config files. When true, + # every downstream job's `needs:` chain skips, and the status-check rollup still + # reports success (it treats `skipped` as passing per its existing logic). + # + # Why this pattern and NOT `paths-ignore:` at the workflow level: + # - `paths-ignore:` skips the whole workflow, which means the required check + # `๐ŸŽฏ All Tests Passed` never reports โ†’ branch protection blocks the merge. + # - The short-circuit pattern keeps the workflow running but causes work jobs + # to skip, so the required check still publishes a green result. + # + # Pull request events resolve the changed-file list via `gh api /pulls/{n}/files` + # (the pre-installed GitHub CLI) โ€” no checkout required, and `--paginate` handles + # PRs with more than 100 changed files. Push events (tags + master/main) always + # short-circuit to `docs-only=false` so the full pipeline runs on those refs. # ---------------------------------------------------------------------------------- - load-env: - name: ๐ŸŒ Load Environment Variables + paths-check: + name: ๐Ÿ“ Paths Check runs-on: ubuntu-24.04 + timeout-minutes: 5 permissions: - contents: read # Read repository content for environment config + contents: read + pull-requests: read # Required: gh api reads PR files via the GitHub REST API (no checkout needed) outputs: - env-json: ${{ steps.load-env.outputs.env-json }} - primary-runner: ${{ steps.load-env.outputs.primary-runner }} - env-file-count: ${{ steps.load-env.outputs.env-file-count }} - var-count: ${{ steps.load-env.outputs.var-count }} + # docs-only is TRUE only when the run contains NO non-docs changes. + # On pull_request events: resolved via gh api /pulls/{n}/files. + # On push events (master/main, tags): always false โ€” full pipeline runs. + docs-only: ${{ steps.filter.outputs.docs-only }} steps: - # -------------------------------------------------------------------- - # Check out code to access env file - # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout code (sparse) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .github/env - .github/actions/load-env + - name: ๐Ÿ” Detect docs-only changes + id: filter + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + EVENT: ${{ github.event_name }} + run: | + if [[ "$EVENT" != "pull_request" ]]; then + echo "docs-only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi - # -------------------------------------------------------------------- - # Load and parse environment file - # -------------------------------------------------------------------- - - name: ๐ŸŒ Load environment variables - uses: ./.github/actions/load-env - id: load-env + # List PR-changed files via the GitHub REST API. --paginate transparently + # handles PRs with more than 100 changed files. + files=$(gh api --paginate "/repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename') + + # Drop any file that matches the docs/config allowlist. If any line + # survives the grep, at least one non-docs file changed. + non_docs=$(printf '%s\n' "$files" | grep -vE '(\.md$|^docs/|^LICENSE$|^\.gitignore$|^CODEOWNERS$|^\.github/ISSUE_TEMPLATE/|^\.github/PULL_REQUEST_TEMPLATE\.md$)' || true) + + if [[ -z "$non_docs" ]]; then + echo "docs-only=true" >> "$GITHUB_OUTPUT" + else + echo "docs-only=false" >> "$GITHUB_OUTPUT" + fi # ---------------------------------------------------------------------------------- - # Setup Configuration Workflow + # Setup Configuration (loads env + runs MAGE-X verification) + # + # The setup-config reusable workflow loads .github/env/ internally and runs the + # MAGE-X verification steps as part of the same job. # ---------------------------------------------------------------------------------- setup: name: ๐Ÿ”ง Setup Configuration - needs: [load-env] + needs: [paths-check] + # Skip the entire pipeline when only docs/config files changed. status-check + # below still runs (it has `if: always()`) and reports green for branch protection. + if: needs.paths-check.outputs.docs-only != 'true' permissions: contents: read # Read repository content for setup configuration uses: ./.github/workflows/fortress-setup-config.yml - with: - env-json: ${{ needs.load-env.outputs.env-json }} - primary-runner: ${{ needs.load-env.outputs.primary-runner }} - env-file-count: ${{ needs.load-env.outputs.env-file-count }} - var-count: ${{ needs.load-env.outputs.var-count }} secrets: github-token: ${{ github.event.pull_request.head.repo.fork != true && (secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN) || '' }} # ---------------------------------------------------------------------------------- - # Test MAGE-X - # ---------------------------------------------------------------------------------- - test-magex: - name: ๐Ÿช„ Verify & Test MAGE-X - needs: [load-env, setup] - permissions: - contents: read # Read repository content for magex testing - uses: ./.github/workflows/fortress-test-magex.yml - with: - env-json: ${{ needs.load-env.outputs.env-json }} - primary-runner: ${{ needs.setup.outputs.primary-runner }} - # ---------------------------------------------------------------------------------- # Warm Go Caches (Secrets optional: only needed when GOPRIVATE is set for private modules) # ---------------------------------------------------------------------------------- warm-cache: name: ๐Ÿ’พ Warm Cache - needs: [load-env, setup, test-magex] + needs: [setup] if: needs.setup.outputs.cache-warming-enabled == 'true' permissions: contents: read # Read repository content for cache warming uses: ./.github/workflows/fortress-warm-cache.yml with: - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} warm-cache-matrix: ${{ needs.setup.outputs.warm-cache-matrix }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} go-secondary-version: ${{ needs.setup.outputs.go-secondary-version }} @@ -157,11 +173,10 @@ jobs: # ---------------------------------------------------------------------------------- security: name: ๐Ÿ”’ Security Scans - needs: [load-env, setup, test-magex, warm-cache] + needs: [setup, warm-cache] if: | !cancelled() && 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.is-fork-pr != 'true' @@ -170,7 +185,7 @@ jobs: pull-requests: write # Required: gitleaks needs to create PR comments uses: ./.github/workflows/fortress-security-scans.yml with: - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} enable-nancy: ${{ needs.setup.outputs.nancy-enabled == 'true' }} enable-govulncheck: ${{ needs.setup.outputs.govulncheck-enabled == 'true' }} enable-gitleaks: ${{ needs.setup.outputs.gitleaks-enabled == 'true' }} @@ -187,18 +202,17 @@ jobs: # ---------------------------------------------------------------------------------- pre-commit: name: ๐Ÿช Pre-commit Checks - needs: [load-env, setup, test-magex, warm-cache] + needs: [setup, warm-cache] if: | !cancelled() && needs.setup.result == 'success' && - needs.test-magex.result == 'success' && (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') && needs.setup.outputs.pre-commit-enabled == 'true' permissions: contents: read # Read repository content for pre-commit checks uses: ./.github/workflows/fortress-pre-commit.yml with: - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} primary-runner: ${{ needs.setup.outputs.primary-runner }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} pre-commit-enabled: ${{ needs.setup.outputs.pre-commit-enabled }} @@ -210,17 +224,16 @@ jobs: # ---------------------------------------------------------------------------------- code-quality: name: ๐Ÿ“Š Code Quality - needs: [load-env, setup, test-magex, warm-cache] + needs: [setup, warm-cache] if: | !cancelled() && needs.setup.result == 'success' && - needs.test-magex.result == 'success' && (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') permissions: contents: read # Read repository content for code quality checks uses: ./.github/workflows/fortress-code-quality.yml with: - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} go-lint-enabled: ${{ needs.setup.outputs.go-lint-enabled }} yaml-lint-enabled: ${{ needs.setup.outputs.yaml-lint-enabled }} @@ -234,11 +247,10 @@ jobs: # ---------------------------------------------------------------------------------- test-suite: name: ๐Ÿงช Test Suite - needs: [load-env, setup, test-magex, warm-cache] + needs: [setup, warm-cache] if: | !cancelled() && needs.setup.result == 'success' && - needs.test-magex.result == 'success' && (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') && needs.setup.outputs.is-fork-pr != 'true' && needs.setup.outputs.go-tests-enabled == 'true' @@ -253,7 +265,7 @@ jobs: with: code-coverage-enabled: ${{ needs.setup.outputs.code-coverage-enabled }} coverage-provider: ${{ needs.setup.outputs.coverage-provider }} - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} fuzz-testing-enabled: ${{ needs.setup.outputs.fuzz-testing-enabled }} go-tests-enabled: ${{ needs.setup.outputs.go-tests-enabled }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} @@ -278,18 +290,17 @@ jobs: # ---------------------------------------------------------------------------------- benchmarks: name: ๐Ÿƒ Benchmarks - needs: [load-env, setup, test-magex, warm-cache] + needs: [setup, warm-cache] if: | !cancelled() && needs.setup.result == 'success' && - needs.test-magex.result == 'success' && (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') && needs.setup.outputs.benchmarks-enabled == 'true' permissions: contents: read # Read repository content for benchmarking uses: ./.github/workflows/fortress-benchmarks.yml with: - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} benchmark-matrix: ${{ needs.setup.outputs.benchmark-matrix }} primary-runner: ${{ needs.setup.outputs.primary-runner }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} @@ -312,18 +323,20 @@ jobs: status-check: name: ๐ŸŽฏ All Tests Passed if: ${{ always() }} - needs: [setup, test-magex, warm-cache, security, code-quality, pre-commit, test-suite, benchmarks] + needs: [paths-check, setup, warm-cache, security, code-quality, pre-commit, test-suite, benchmarks] permissions: contents: read # Read repository content for status checking - runs-on: ${{ needs.setup.outputs.primary-runner }} + # Fallback to ubuntu-latest when setup was skipped (e.g. docs-only PR via paths-check) + # so this required check can still report a green result for branch protection. + runs-on: ${{ needs.setup.outputs.primary-runner || 'ubuntu-24.04' }} steps: # -------------------------------------------------------------------- # Build results summary showing job statuses # -------------------------------------------------------------------- - name: ๐Ÿ“Š Build results summary env: + PATHS_RESULT: ${{ needs.paths-check.result }} SETUP_RESULT: ${{ needs.setup.result }} - MAGEX_RESULT: ${{ needs.test-magex.result }} CACHE_RESULT: ${{ needs.warm-cache.result }} SECURITY_RESULT: ${{ needs.security.result }} QUALITY_RESULT: ${{ needs.code-quality.result }} @@ -357,13 +370,15 @@ jobs: fi } + # Paths Check (docs-only short-circuit gate) + PATHS_DISPLAY=$(get_result_display "$PATHS_RESULT") + echo "| ๐Ÿ“ Paths Check | $PATHS_DISPLAY | Required |" + # Setup SETUP_DISPLAY=$(get_result_display "$SETUP_RESULT") echo "| ๐ŸŽฏ Setup | $SETUP_DISPLAY | Required |" - # MAGE-X - MAGEX_DISPLAY=$(get_result_display "$MAGEX_RESULT") - echo "| ๐Ÿช„ MAGE-X | $MAGEX_DISPLAY | Required |" + # MAGE-X verification is now a step inside Setup, no separate row needed # Warm Cache CACHE_REQ="Disabled" @@ -413,14 +428,18 @@ jobs: run: | FAILED=false - # Check required jobs (these must pass) - if [[ "${{ needs.setup.result }}" == "failure" || "${{ needs.setup.result }}" == "cancelled" ]]; then - echo "โŒ Setup failed or was cancelled" >&2 + # Path detection must succeed. If it failed/was cancelled, the docs-only + # short-circuit could not be computed and the whole pipeline was skipped on a + # false premise โ€” this required check must NOT report green in that case. + # (The intentional docs-only path leaves paths-check 'success', so it passes.) + if [[ "${{ needs.paths-check.result }}" == "failure" || "${{ needs.paths-check.result }}" == "cancelled" ]]; then + echo "โŒ Paths check failed or was cancelled" >&2 FAILED=true fi - if [[ "${{ needs.test-magex.result }}" == "failure" || "${{ needs.test-magex.result }}" == "cancelled" ]]; then - echo "โŒ Test MAGE-X failed or was cancelled" >&2 + # Check required jobs (these must pass) + if [[ "${{ needs.setup.result }}" == "failure" || "${{ needs.setup.result }}" == "cancelled" ]]; then + echo "โŒ Setup failed or was cancelled" >&2 FAILED=true fi @@ -478,7 +497,7 @@ jobs: # ---------------------------------------------------------------------------------- release: name: ๐Ÿš€ Release Version - needs: [load-env, setup, test-magex, test-suite, security, code-quality, pre-commit] + needs: [setup, test-suite, security, code-quality, pre-commit] # Only run on successful tag pushes from same repository (not forks) # Allow release even if test-suite was skipped (when ENABLE_GO_TESTS=false) if: | @@ -486,14 +505,13 @@ jobs: startsWith(github.ref, 'refs/tags/v') && needs.setup.outputs.is-fork-pr != 'true' && needs.setup.result == 'success' && - needs.test-magex.result == 'success' && (needs.test-suite.result == 'success' || needs.test-suite.result == 'skipped') && needs.security.result == 'success' && needs.code-quality.result == 'success' && needs.pre-commit.result == 'success' uses: ./.github/workflows/fortress-release.yml with: - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} primary-runner: ${{ needs.setup.outputs.primary-runner }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} golangci-lint-version: ${{ needs.code-quality.outputs.golangci-lint-version }} @@ -510,11 +528,9 @@ jobs: name: ๐Ÿ“Š Workflow Completion Report if: | always() && - needs.load-env.result == 'success' && needs.setup.result == 'success' && - needs.test-magex.result == 'success' && needs.setup.outputs.completion-report-enabled == 'true' - needs: [load-env, setup, test-magex, pre-commit, security, code-quality, test-suite, benchmarks, release, status-check] + needs: [setup, pre-commit, security, code-quality, test-suite, benchmarks, release, status-check] permissions: contents: read # Read repository content for completion report actions: read # Required for artifact downloads @@ -523,7 +539,7 @@ jobs: benchmarks-result: ${{ needs.benchmarks.result }} code-quality-result: ${{ needs.code-quality.result }} pre-commit-result: ${{ needs.pre-commit.result }} - env-json: ${{ needs.load-env.outputs.env-json }} + env-json: ${{ needs.setup.outputs.env-json }} primary-runner: ${{ needs.setup.outputs.primary-runner }} release-result: ${{ needs.release.result }} security-result: ${{ needs.security.result }} @@ -531,7 +547,6 @@ jobs: start-epoch: ${{ needs.setup.outputs.start-epoch }} start-time: ${{ needs.setup.outputs.start-time }} status-check-result: ${{ needs.status-check.result }} - test-magex-result: ${{ needs.test-magex.result }} test-matrix: ${{ needs.setup.outputs.test-matrix }} test-suite-result: ${{ needs.test-suite.result }} gofortress-version: ${{ needs.setup.outputs.gofortress-version }} diff --git a/.github/workflows/pull-request-management-fork.yml b/.github/workflows/pull-request-management-fork.yml deleted file mode 100644 index d32bfbc..0000000 --- a/.github/workflows/pull-request-management-fork.yml +++ /dev/null @@ -1,536 +0,0 @@ -# ------------------------------------------------------------------------------------ -# Pull Request Management for Forks Workflow -# -# Purpose: Automate labeling, assignment, and welcoming of pull requests for forked PRs. -# -# Configuration: All settings are loaded from modular .github/env/*.env 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 - Two-Workflow Pattern for Safe Fork PR Handling -# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -# -# This workflow implements the RECOMMENDED security pattern for handling fork PRs -# as documented in GitHub Security Best Practices (githubactions:S7631). -# -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚ WHY pull_request_target IS SAFE HERE: โ”‚ -# โ”‚ โ”‚ -# โ”‚ โœ… Uses pull_request_target trigger for write permissions โ”‚ -# โ”‚ (Required for: labels, comments, assignees) โ”‚ -# โ”‚ โ”‚ -# โ”‚ โœ… CRITICAL: Only checks out BASE branch code, NEVER PR head โ”‚ -# โ”‚ (Prevents malicious code execution from untrusted forks) โ”‚ -# โ”‚ โ”‚ -# โ”‚ โœ… 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 code from PR is ever executed) โ”‚ -# โ”‚ โ”‚ -# โ”‚ โœ… No secrets exposed to fork PRs (GITHUB_TOKEN only) โ”‚ -# โ”‚ (No custom secrets accessible to malicious actors) โ”‚ -# โ”‚ โ”‚ -# โ”‚ โœ… Sparse checkout minimizes attack surface โ”‚ -# โ”‚ (Only config files checked out, no executable code) โ”‚ -# โ”‚ โ”‚ -# โ”‚ โœ… Least-privilege permissions model โ”‚ -# โ”‚ (Jobs get elevated permissions only where absolutely needed) โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# -# SECURITY PATTERN: Two-Workflow Approach -# โ”œโ”€ pull-request-management.yml โ†’ Same-repo PRs (uses pull_request) -# โ””โ”€ pull-request-management-fork.yml โ†’ Fork PRs (uses pull_request_target) -# -# WHAT COULD GO WRONG (and how we prevent it): -# โŒ Malicious fork creates PR with code that steals secrets -# โœ… PREVENTED: We never checkout or execute PR code -# -# โŒ Attacker modifies workflow files in their fork -# โœ… PREVENTED: pull_request_target runs base repo workflow only -# -# โŒ Malicious code in PR tries to access repository secrets -# โœ… PREVENTED: Only GITHUB_TOKEN exposed, no custom secrets -# -# โŒ Code injection via PR title/description into workflow -# โœ… PREVENTED: All user input properly escaped in GitHub Actions -# -# SECURITY SCANNERS: -# - GitHub Security: May flag pull_request_target + checkout (FALSE POSITIVE) -# - Semgrep: May flag dangerous-checkout pattern (FALSE POSITIVE) -# - Checkov: May flag CKV_GHA_3 (FALSE POSITIVE) -# -# These are FALSE POSITIVES because: -# 1. We explicitly checkout base branch, not PR head -# 2. This is the RECOMMENDED pattern per GitHub docs -# 3. All security controls are properly implemented -# -# REFERENCES: -# - GitHub Security Advisory: githubactions:S7631 -# - GitHub Docs: Keeping your GitHub Actions and workflows secure -# - OWASP: CI/CD Security Top 10 -# -# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -# -# ------------------------------------------------------------------------------------ - -name: PR Management (Forks) - -# -------------------------------------------------------------------- -# Trigger Configuration -# -------------------------------------------------------------------- -on: - pull_request_target: - types: [opened, reopened, ready_for_review, closed, synchronize] - -# Security: Restrict default permissions (jobs must explicitly request what they need) -permissions: {} - -# -------------------------------------------------------------------- -# Concurrency Control -# -------------------------------------------------------------------- -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }}-fork - cancel-in-progress: true - -# -------------------------------------------------------------------- -# Environment Variables -# -------------------------------------------------------------------- -# Note: Configuration variables are loaded from modular .github/env/*.env files - -jobs: - # ------------------------------------------------------------ - # Load env from the BASE repo only (safe) for centralized config - # ------------------------------------------------------------ - load-env: - name: ๐ŸŒ Load Environment (Base Repo) - runs-on: ubuntu-latest - # Only run for fork PRs - same-repo PRs are handled by pull-request-management.yml - if: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name != github.repository }} - # No write perms here - permissions: - contents: read - outputs: - env-json: ${{ steps.load-env.outputs.env-json }} - steps: - # โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - # โ”‚ SECURITY SCANNERS: This checkout is SAFE despite pull_request_targetโ”‚ - # โ”‚ โ”‚ - # โ”‚ Justification: โ”‚ - # โ”‚ - Only checks out TRUSTED base branch (ref: github.base_ref) โ”‚ - # โ”‚ - NEVER checks out PR head code from untrusted fork โ”‚ - # โ”‚ - Implements recommended two-workflow security pattern โ”‚ - # โ”‚ - Uses sparse checkout (minimal attack surface) โ”‚ - # โ”‚ - No executable code from PR is ever run โ”‚ - # โ”‚ โ”‚ - # โ”‚ Pattern: Two-workflow security model (see SECURITY.md) โ”‚ - # โ”‚ References: githubactions:S7631, semgrep:github-actions-checkout โ”‚ - # โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - # semgrep:ignore github-actions-dangerous-checkout - # codeql:ignore GH001 - # checkov:skip=CKV_GHA_3:Base branch checkout is intentional and safe - - name: ๐Ÿ“ฅ Checkout base repo (sparse) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” - # ๐Ÿ”’ CRITICAL SECURITY CONTROL: Base Branch Checkout Only - # โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” - # This workflow uses pull_request_target for write permissions BUT - # ONLY checks out the trusted base branch code - NEVER PR head code. - # - # WHY THIS IS SAFE: - # - ref parameter explicitly set to base branch (github.base_ref) - # - Malicious fork PRs cannot inject code into this workflow - # - All code execution happens from trusted repository only - # - Sparse checkout limits to config files only (no executables) - # - # SECURITY MODEL: - # - pull_request_target = write permissions needed for labels/comments - # - Base branch checkout = prevents malicious code execution - # - This is the RECOMMENDED pattern for fork PR automation - # โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” - ref: ${{ github.base_ref }} - fetch-depth: 1 - sparse-checkout: | - .github/env - .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 - # Only run for fork PRs - same-repo PRs are handled by pull-request-management.yml - if: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name != github.repository }} - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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 - # Only run for fork PRs, but always show summary regardless of job status - if: always() && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name != github.repository - 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 c55e743..7e9decb 100644 --- a/.github/workflows/pull-request-management.yml +++ b/.github/workflows/pull-request-management.yml @@ -1,31 +1,84 @@ # ------------------------------------------------------------------------------------ # Pull Request Management Workflow # -# Purpose: Comprehensive PR lifecycle management including automated labeling, -# assignments, size analysis, welcomes for new contributors, and cleanup -# tasks when PRs are closed. All configuration is centralized and customizable. +# Purpose: Comprehensive PR lifecycle management for BOTH same-repo and fork PRs: +# automated labeling, assignments, size analysis, welcome messages, cache cleanup, +# and branch deletion. All configuration is centralized in modular .github/env/ files. # -# Configuration: All settings are loaded from modular .github/env/ files for -# centralized management across all workflows. +# Triggers: pull_request_target (a single trigger covering both same-repo and fork PRs). # -# Triggers: Pull request events (opened, reopened, ready for review, closed, synchronize) +# Maintainer: @mrz1836 # -# 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 +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# ๐Ÿ”’ SECURITY MODEL โ€” Single-Workflow / Two-Job Pattern +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # -# Maintainer: @mrz1836 +# This workflow handles both same-repo and fork PRs from a single file on the +# `pull_request_target` trigger. Using one trigger for both is safe because NEITHER +# path ever executes PR code โ€” every action goes through the GitHub REST API. +# +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ WHY USING pull_request_target FOR BOTH IS SAFE: โ”‚ +# โ”‚ โ”‚ +# โ”‚ โœ… Trigger always evaluates the workflow file from the BASE repository. โ”‚ +# โ”‚ A malicious fork cannot modify this workflow to elevate privileges. โ”‚ +# โ”‚ โ”‚ +# โ”‚ โœ… Checkout ALWAYS uses `ref: ${{ github.base_ref }}` and a sparse pattern โ”‚ +# โ”‚ limited to read-only config files (.github/env, .github/actions/...). โ”‚ +# โ”‚ PR head code is NEVER checked out and NEVER executed. โ”‚ +# โ”‚ โ”‚ +# โ”‚ โœ… All write operations are explicit, hard-coded GitHub REST API calls โ”‚ +# โ”‚ (labels / assignees / comments / cache delete / ref delete). No shell โ”‚ +# โ”‚ command derives its arguments from PR-controlled data without first โ”‚ +# โ”‚ being routed through `process.env.*` (preventing shell injection). โ”‚ +# โ”‚ โ”‚ +# โ”‚ โœ… Only GITHUB_TOKEN is exposed. No custom secrets are referenced. โ”‚ +# โ”‚ โ”‚ +# โ”‚ โœ… Fork detection uses head.repo.full_name (handles deleted forks safely โ”‚ +# โ”‚ via the `head.repo &&` guard โ€” null head.repo means neither job runs). โ”‚ +# โ”‚ โ”‚ +# โ”‚ โœ… Least-privilege per execution path: same-repo PRs need `contents:write` โ”‚ +# โ”‚ for branch deletion, fork PRs do NOT. The two-job split below preserves โ”‚ +# โ”‚ that distinction โ€” fork PRs run with the minimum permissions necessary โ”‚ +# โ”‚ even though everything lives in one file. โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# +# Job structure (mutually exclusive โ€” exactly one runs per PR, the other skips): +# โ”œโ”€ pr-management-same-repo โ†’ head.repo.full_name == github.repository +# โ”‚ Permissions: actions:write, contents:write, pull-requests:write +# โ”‚ Work: type labels, default assignee, first-timer welcome, size label, +# โ”‚ cache cleanup on close, branch deletion on merge. +# โ”‚ +# โ””โ”€ pr-management-fork โ†’ head.repo.full_name != github.repository +# Permissions: actions:write, issues:write, pull-requests:write +# (NOT contents:write โ€” fork branches can't be deleted from base) +# Work: fork+triage labels, default assignee, fork welcome notice, +# cache cleanup on close. NO branch deletion. NO type labels โ€” +# those require pre-merge code review. +# +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# ๐Ÿ” WHY pull_request_target ALARMS SECURITY SCANNERS (FALSE POSITIVE) +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # -# 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 +# Scanners (Semgrep, Checkov, CodeQL) flag pull_request_target + actions/checkout +# as a high-severity finding because the COMBINATION can leak the elevated token +# to malicious fork code. The pattern is documented to be DANGEROUS WHEN PR HEAD +# IS CHECKED OUT. +# +# This workflow does NOT check out PR head โ€” only the BASE branch (`github.base_ref`) +# via sparse checkout of read-only config files. The pattern is therefore SAFE +# per the official GitHub security guidance. +# +# Suppressions: +# - Semgrep: github-actions-dangerous-checkout (false positive) +# - Checkov: CKV_GHA_3 (false positive) +# - CodeQL: GH001 (false positive) +# - Guardian: see .github/guardian.yaml exception entry +# +# References: +# - GitHub Docs: Keeping your GitHub Actions and workflows secure โ€” Preventing +# pwn requests (https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) +# - GitHub Security Advisory: githubactions:S7631 # # ------------------------------------------------------------------------------------ @@ -33,142 +86,119 @@ name: PR Management # -------------------------------------------------------------------- # Trigger Configuration +# +# pull_request_target runs from the BASE repository regardless of source. +# This gives us a write-capable GITHUB_TOKEN for both same-repo and fork PRs +# while guaranteeing the workflow file itself is never the fork's copy. +# +# `synchronize` is intentionally NOT included. It fires on every push to a PR +# and the only work it would do (re-applying labels + re-checking the default +# assignee) is idempotent โ€” both persist from the `opened` run. Skipping it +# lets maintainers manually override auto-applied labels without the workflow +# fighting back on the next commit. # -------------------------------------------------------------------- on: - pull_request: - types: [opened, reopened, ready_for_review, closed, synchronize] + pull_request_target: + types: [opened, reopened, ready_for_review, closed] -# Security: Restrict default permissions (jobs must explicitly request what they need) +# Security: Workflow-level permissions are zeroed. Each job below requests +# only what it strictly needs. permissions: {} # -------------------------------------------------------------------- # Concurrency Control +# +# One group per PR โ€” a new event (e.g., synchronize) cancels in-flight runs +# for the same PR. The two jobs below share this group implicitly since they +# belong to the same workflow. # -------------------------------------------------------------------- concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true -# -------------------------------------------------------------------- -# Environment Variables -# -------------------------------------------------------------------- -# Note: Configuration variables are loaded from modular .github/env/ files - jobs: - # ---------------------------------------------------------------------------------- - # Load Environment Variables - # ---------------------------------------------------------------------------------- - load-env: - name: ๐ŸŒ Load Environment Variables - runs-on: ubuntu-latest - # Early exit: Skip entire workflow for fork PRs (handled by fork workflow) - if: github.event.pull_request.head.repo.full_name == github.repository + # ==================================================================================== + # Same-Repo PRs + # + # Runs when the PR head and base point at the same repository (trusted contributor). + # Performs the full PR management lifecycle including branch deletion on merge. + # ==================================================================================== + pr-management-same-repo: + name: ๐Ÿ”ง PR Management (Same Repo) + if: github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + timeout-minutes: 10 permissions: - contents: read # Required: Read repository content for sparse checkout - outputs: - env-json: ${{ steps.load-env.outputs.env-json }} + actions: write # Required: Delete GitHub Actions caches for closed PRs + contents: write # Required: Delete merged branches from the base repo + pull-requests: write # Required: Apply labels, assign reviewers, post comments + steps: # -------------------------------------------------------------------- - # Check out code to access env file + # SECURITY-CRITICAL CHECKOUT โ€” explicit base-ref + sparse + no fetch-depth + # + # pull_request_target's checkout already defaults to the base branch, but + # we set `ref` explicitly to make the intent unmistakable and to harden + # against accidental changes (e.g., a future contributor swapping the + # checkout for a "let's just check out the PR for convenience" version). # -------------------------------------------------------------------- - - name: ๐Ÿ“ฅ Checkout code (sparse) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # semgrep:ignore github-actions-dangerous-checkout + # codeql:ignore GH001 + # checkov:skip=CKV_GHA_3:Base branch checkout is intentional and safe + # sonarcloud:S7631 โ€” false positive: base-ref sparse checkout only (see NOSONAR below) + - name: ๐Ÿ“ฅ Checkout base repo (sparse) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 โ€” NOSONAR(S7631): base-ref sparse checkout only; PR head is never checked out or executed with: + persist-credentials: false + ref: ${{ github.base_ref || github.ref }} + fetch-depth: 1 sparse-checkout: | .github/env .github/actions/load-env - # -------------------------------------------------------------------- - # Load and parse environment file - # -------------------------------------------------------------------- - name: ๐ŸŒ Load environment variables - uses: ./.github/actions/load-env id: load-env + uses: ./.github/actions/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, 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' && - needs.detect-same-repo.outputs.is-same-repo == 'true' - outputs: - labels-applied: ${{ steps.apply-labels.outputs.labels-applied }} - - steps: # -------------------------------------------------------------------- - # Extract configuration from env-json + # Extract all PR-management configuration up front (single jq pass). + # All variables passed downstream via $GITHUB_ENV. # -------------------------------------------------------------------- - name: ๐Ÿ”ง Extract configuration - id: config env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | - echo "๐Ÿ“‹ Extracting PR management configuration from environment..." - - # Extract all needed variables - SKIP_BOT_USERS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SKIP_BOT_USERS') - APPLY_TYPE_LABELS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_TYPE_LABELS') + echo "๐Ÿ“‹ Extracting PR management configuration..." + + { + echo "SKIP_BOT_USERS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SKIP_BOT_USERS')" + echo "APPLY_TYPE_LABELS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_TYPE_LABELS')" + echo "APPLY_SIZE_LABELS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_SIZE_LABELS')" + echo "DEFAULT_ASSIGNEE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE')" + echo "WELCOME_FIRST_TIME=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FIRST_TIME')" + echo "SIZE_XS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_XS_THRESHOLD')" + echo "SIZE_S=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_S_THRESHOLD')" + echo "SIZE_M=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_M_THRESHOLD')" + echo "SIZE_L=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_L_THRESHOLD')" + echo "CLEAN_CACHE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_CLEAN_CACHE_ON_CLOSE')" + echo "DELETE_BRANCH=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DELETE_BRANCH_ON_MERGE')" + echo "PROTECTED_BRANCHES=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_PROTECTED_BRANCHES')" + } >> "$GITHUB_ENV" - # Set as environment variables for all subsequent steps - echo "SKIP_BOT_USERS=$SKIP_BOT_USERS" >> $GITHUB_ENV - echo "APPLY_TYPE_LABELS=$APPLY_TYPE_LABELS" >> $GITHUB_ENV - - # Log configuration echo "๐Ÿ” Configuration loaded:" - echo " ๐Ÿค– Skip bot users: $SKIP_BOT_USERS" - echo " ๐Ÿท๏ธ Apply type labels: $APPLY_TYPE_LABELS" + echo " ๐Ÿท๏ธ Apply type labels: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_TYPE_LABELS')" + echo " ๐Ÿ“ Apply size labels: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_SIZE_LABELS')" + echo " ๐Ÿ‘ค Default assignee: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE')" + echo " ๐Ÿ‘‹ Welcome first-time contributors: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FIRST_TIME')" + echo " ๐Ÿงน Clean cache on close: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_CLEAN_CACHE_ON_CLOSE')" + echo " ๐ŸŒฟ Delete branch on merge: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DELETE_BRANCH_ON_MERGE')" # -------------------------------------------------------------------- - # Apply labels based on branch and title patterns + # Apply branch/title-based labels (chore, feature, bug, etc.) # -------------------------------------------------------------------- - name: ๐Ÿท๏ธ Apply labels based on patterns id: apply-labels - if: env.APPLY_TYPE_LABELS == 'true' + if: github.event.action != 'closed' && env.APPLY_TYPE_LABELS == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -225,10 +255,8 @@ jobs: { pattern: /\b(wip|work.in.progress|draft|incomplete)\b/i, labels: ['work-in-progress'] }, ]; - // Collect labels from both branch and title - const labelsToAdd = new Set(); // Use Set to avoid duplicates + const labelsToAdd = new Set(); - // Check branch patterns console.log('๐ŸŒฟ Checking branch patterns...'); for (const rule of branchRules) { if (rule.pattern.test(branch)) { @@ -237,7 +265,6 @@ jobs: } } - // Check title patterns console.log('๐Ÿ“ Checking title patterns...'); for (const rule of titleRules) { if (rule.pattern.test(prTitle)) { @@ -257,7 +284,6 @@ jobs: console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); console.log(`๐Ÿ“‹ Total labels to apply: ${finalLabels.join(', ')}`); - // Get existing labels to avoid duplicates try { const { data: existingLabels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, @@ -293,52 +319,12 @@ jobs: // Don't fail the entire workflow for label issues } - # ---------------------------------------------------------------------------------- - # Assign Default Assignee - # ---------------------------------------------------------------------------------- - assign-assignee: - name: ๐Ÿ‘ค Assign Default Assignee - 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' && - needs.detect-same-repo.outputs.is-same-repo == 'true' - outputs: - assignee-added: ${{ steps.assign.outputs.assignee-added }} - - 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 - DEFAULT_ASSIGNEE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE') - SKIP_BOT_USERS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SKIP_BOT_USERS') - - # Set as environment variables for all subsequent steps - echo "DEFAULT_ASSIGNEE=$DEFAULT_ASSIGNEE" >> $GITHUB_ENV - echo "SKIP_BOT_USERS=$SKIP_BOT_USERS" >> $GITHUB_ENV - - # Log configuration - echo "๐Ÿ” Configuration loaded:" - echo " ๐Ÿ‘ค Default assignee: $DEFAULT_ASSIGNEE" - echo " ๐Ÿค– Skip bot users: $SKIP_BOT_USERS" - - # -------------------------------------------------------------------- - # Assign default assignee if needed + # Assign default assignee if PR has none # -------------------------------------------------------------------- - name: ๐Ÿ‘ค Assign default assignee - id: assign + id: assign-assignee + if: github.event.action != 'closed' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -347,7 +333,6 @@ jobs: const prAuthor = pr.user.login; const assignees = pr.assignees || []; - // Check if PR author is a bot to skip const skipBotUsers = process.env.SKIP_BOT_USERS.split(',').map(u => u.trim()); if (skipBotUsers.includes(prAuthor)) { console.log(`โญ๏ธ Skipping assignment for bot user: ${prAuthor}`); @@ -379,53 +364,16 @@ jobs: // Don't fail the workflow for assignment issues } - # ---------------------------------------------------------------------------------- - # Welcome New Contributors - # ---------------------------------------------------------------------------------- - welcome-contributor: - name: ๐Ÿ‘‹ Welcome New Contributor - needs: [load-env, detect-same-repo] - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - if: | - github.event.action == 'opened' && - contains(fromJSON('["FIRST_TIMER", "FIRST_TIME_CONTRIBUTOR"]'), github.event.pull_request.author_association) && - needs.detect-same-repo.outputs.is-same-repo == 'true' - outputs: - welcomed: ${{ steps.welcome.outputs.welcomed }} - - 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 - WELCOME_FIRST_TIME=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FIRST_TIME') - SKIP_BOT_USERS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SKIP_BOT_USERS') - - # Set as environment variables for all subsequent steps - echo "WELCOME_FIRST_TIME=$WELCOME_FIRST_TIME" >> $GITHUB_ENV - echo "SKIP_BOT_USERS=$SKIP_BOT_USERS" >> $GITHUB_ENV - - # Log configuration - echo "๐Ÿ” Configuration loaded:" - echo " ๐Ÿ‘‹ Welcome first-time contributors: $WELCOME_FIRST_TIME" - echo " ๐Ÿค– Skip bot users: $SKIP_BOT_USERS" - # -------------------------------------------------------------------- - # Post welcome message + # Welcome first-time contributors (same-repo only โ€” fork PRs receive + # a different, security-focused welcome in the fork job below). # -------------------------------------------------------------------- - name: ๐Ÿ‘‹ Welcome new contributor - id: welcome - if: env.WELCOME_FIRST_TIME == 'true' + id: welcome-contributor + if: | + github.event.action == 'opened' && + contains(fromJSON('["FIRST_TIMER", "FIRST_TIME_CONTRIBUTOR"]'), github.event.pull_request.author_association) && + env.WELCOME_FIRST_TIME == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -434,7 +382,6 @@ jobs: const repoName = context.repo.repo; const repoOwner = context.repo.owner; - // Check if PR author is a bot to skip const skipBotUsers = process.env.SKIP_BOT_USERS.split(',').map(u => u.trim()); if (skipBotUsers.includes(author)) { console.log(`โญ๏ธ Skipping welcome for bot user: ${author}`); @@ -472,59 +419,12 @@ jobs: core.setOutput('welcomed', 'false'); } - # ---------------------------------------------------------------------------------- - # Analyze PR Size - # ---------------------------------------------------------------------------------- - analyze-size: - name: ๐Ÿ“ Analyze PR Size - needs: [load-env, detect-same-repo] - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - 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 }} - - 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 - APPLY_SIZE_LABELS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_SIZE_LABELS') - SIZE_XS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_XS_THRESHOLD') - SIZE_S=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_S_THRESHOLD') - SIZE_M=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_M_THRESHOLD') - SIZE_L=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SIZE_L_THRESHOLD') - - # Set as environment variables for all subsequent steps - echo "APPLY_SIZE_LABELS=$APPLY_SIZE_LABELS" >> $GITHUB_ENV - echo "SIZE_XS=$SIZE_XS" >> $GITHUB_ENV - echo "SIZE_S=$SIZE_S" >> $GITHUB_ENV - echo "SIZE_M=$SIZE_M" >> $GITHUB_ENV - echo "SIZE_L=$SIZE_L" >> $GITHUB_ENV - - # Log configuration - echo "๐Ÿ” Configuration loaded:" - echo " ๐Ÿ“ Apply size labels: $APPLY_SIZE_LABELS" - echo " ๐Ÿ“Š Size thresholds: XSโ‰ค$SIZE_XS, Sโ‰ค$SIZE_S, Mโ‰ค$SIZE_M, Lโ‰ค$SIZE_L, XL>$SIZE_L" - # -------------------------------------------------------------------- - # Analyze and label PR size + # PR size analysis + size/XS|S|M|L|XL label (opened events only) # -------------------------------------------------------------------- - name: ๐Ÿ“ Add size label - id: analyze - if: env.APPLY_SIZE_LABELS == 'true' + id: analyze-size + if: github.event.action == 'opened' && env.APPLY_SIZE_LABELS == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -539,7 +439,6 @@ jobs: console.log(` โž– Deletions: ${deletions}`); console.log(` ๐Ÿ“ˆ Total changes: ${totalChanges}`); - // Determine size label based on configurable thresholds let sizeLabel = ''; const thresholds = { XS: parseInt(process.env.SIZE_XS), @@ -578,47 +477,12 @@ jobs: core.setOutput('total-changes', totalChanges.toString()); } - # ---------------------------------------------------------------------------------- - # Clean Runner Cache (on PR close) - # ---------------------------------------------------------------------------------- - clean-cache: - name: ๐Ÿงน Clean Runner Cache - needs: [load-env, detect-same-repo] - 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' - 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') - - # 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 + # Cache cleanup on PR close (frees up GH Actions cache quota) # -------------------------------------------------------------------- - name: ๐Ÿงน Cleanup caches - id: clean - if: env.CLEAN_CACHE == 'true' + id: clean-cache + if: github.event.action == 'closed' && env.CLEAN_CACHE == 'true' env: PR_NUMBER: ${{ github.event.pull_request.number }} PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} @@ -628,19 +492,16 @@ jobs: echo "๐Ÿงน Cleaning up caches for 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) + # PR_HEAD_REF is read from env (not interpolated into the jq filter) + # to prevent jq-injection via crafted branch names. cacheKeysForPR=$(echo "$allCaches" | jq -r --arg pr "$PR_NUMBER" --arg branch "$PR_HEAD_REF" \ '.[] | select( .ref == "refs/pull/\($pr)/merge" or @@ -648,7 +509,6 @@ jobs: .ref == "refs/heads/\($branch)" ) | .id') - # Count caches - handle empty results properly if [ -z "$cacheKeysForPR" ]; then cacheCount=0 else @@ -663,11 +523,9 @@ jobs: 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" @@ -681,81 +539,40 @@ jobs: echo "โœ… Cleaned $cleanedCount out of $cacheCount cache(s)" echo "caches-cleaned=$cleanedCount" >> $GITHUB_OUTPUT - # ---------------------------------------------------------------------------------- - # Delete Merged Branch - # ---------------------------------------------------------------------------------- - delete-branch: - name: ๐ŸŒฟ Delete Merged Branch - 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 && - needs.detect-same-repo.outputs.is-same-repo == 'true' - outputs: - branch-deleted: ${{ steps.delete.outputs.branch-deleted }} - - 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 - DELETE_BRANCH=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DELETE_BRANCH_ON_MERGE') - PROTECTED_BRANCHES=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_PROTECTED_BRANCHES') - - # Set as environment variables for all subsequent steps - echo "DELETE_BRANCH=$DELETE_BRANCH" >> $GITHUB_ENV - echo "PROTECTED_BRANCHES=$PROTECTED_BRANCHES" >> $GITHUB_ENV - - # Log configuration - echo "๐Ÿ” Configuration loaded:" - echo " ๐Ÿ—‘๏ธ Delete branch on merge: $DELETE_BRANCH" - echo " ๐Ÿ”’ Protected branches: $PROTECTED_BRANCHES" - - # -------------------------------------------------------------------- - # Delete the merged branch + # Delete the merged branch (same-repo only โ€” branches in forks can't + # be deleted from the base repo, hence this step is absent from the + # fork job below). # -------------------------------------------------------------------- - name: ๐ŸŒฟ Delete branch - id: delete - if: env.DELETE_BRANCH == 'true' + id: delete-branch + if: | + github.event.action == 'closed' && + github.event.pull_request.merged == true && + env.DELETE_BRANCH == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - // Get repo owner, name, and branch to delete const owner = context.repo.owner; const repo = context.repo.repo; const branch = context.payload.pull_request.head.ref; console.log(`๐ŸŒฟ Processing branch deletion for: ${branch}`); - // Fetch repository data to determine the default branch const { data: repoData } = await github.rest.repos.get({ owner, repo, }); const defaultBranch = repoData.default_branch; - // Build list of protected branches from config and default const configProtected = process.env.PROTECTED_BRANCHES.split(',').map(b => b.trim()); const protectedBranches = [...new Set([...configProtected, defaultBranch])]; console.log(`๐Ÿ”’ Protected branches: ${protectedBranches.join(', ')}`); - // Only delete if not a protected branch if (!protectedBranches.includes(branch)) { try { - // Attempt to delete the branch ref await github.rest.git.deleteRef({ owner, repo, @@ -764,12 +581,10 @@ jobs: console.log(`โœ… Deleted branch: ${branch}`); core.setOutput('branch-deleted', 'true'); } catch (error) { - // Handle case where branch is already deleted or protected if (error.status === 422) { console.log(`โ„น๏ธ Branch ${branch} already deleted or protected`); core.setOutput('branch-deleted', 'false'); } else { - // Fail the workflow for other errors console.error(`โŒ Failed to delete branch ${branch}: ${error.message}`); core.setOutput('branch-deleted', 'false'); core.setFailed(`Failed to delete branch ${branch}: ${error.message}`); @@ -780,229 +595,469 @@ jobs: core.setOutput('branch-deleted', 'skip'); } - # ---------------------------------------------------------------------------------- - # Generate Workflow Summary Report - # ---------------------------------------------------------------------------------- - summary: - name: ๐Ÿ“Š Generate Summary - if: always() - needs: [load-env, detect-same-repo, apply-labels, assign-assignee, welcome-contributor, analyze-size, clean-cache, delete-branch] - runs-on: ubuntu-latest - steps: # -------------------------------------------------------------------- - # Generate a workflow summary report + # Workflow summary # -------------------------------------------------------------------- - name: ๐Ÿ“Š Generate workflow summary + if: always() env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} 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 }} + APPLY_LABELS_OUTCOME: ${{ steps.apply-labels.outcome }} + APPLY_LABELS_OUTPUT: ${{ steps.apply-labels.outputs.labels-applied }} + ASSIGN_OUTCOME: ${{ steps.assign-assignee.outcome }} + ASSIGN_OUTPUT: ${{ steps.assign-assignee.outputs.assignee-added }} + WELCOME_OUTCOME: ${{ steps.welcome-contributor.outcome }} + WELCOME_OUTPUT: ${{ steps.welcome-contributor.outputs.welcomed }} + SIZE_OUTCOME: ${{ steps.analyze-size.outcome }} + SIZE_LABEL: ${{ steps.analyze-size.outputs.size-label }} + TOTAL_CHANGES: ${{ steps.analyze-size.outputs.total-changes }} + CACHE_OUTCOME: ${{ steps.clean-cache.outcome }} + CACHES_CLEANED: ${{ steps.clean-cache.outputs.caches-cleaned }} + DELETE_OUTCOME: ${{ steps.delete-branch.outcome }} + BRANCH_DELETED: ${{ steps.delete-branch.outputs.branch-deleted }} run: | echo "๐Ÿ“Š Generating workflow summary..." - echo "# ๐Ÿ”ง Pull Request Management Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**โฐ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY - echo "**๐Ÿ“‹ PR:** #$PR_NUMBER - $PR_TITLE" >> $GITHUB_STEP_SUMMARY - echo "**๐ŸŽฌ Action:** $PR_ACTION" >> $GITHUB_STEP_SUMMARY - echo "**๐Ÿ‘ค Author:** @$PR_AUTHOR" >> $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 + { + echo "# ๐Ÿ”ง Pull Request Management Summary (Same Repo)" + echo "" + echo "**โฐ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "**๐Ÿ“‹ PR:** #$PR_NUMBER - $PR_TITLE" + echo "**๐ŸŽฌ Action:** $PR_ACTION" + echo "**๐Ÿ‘ค Author:** @$PR_AUTHOR" + echo "**๐Ÿ”’ PR Type:** Same-repo (trusted contributor)" + 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 - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Action | Result |" >> $GITHUB_STEP_SUMMARY - echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY - - # Labels applied - if [ "${{ needs.apply-labels.result }}" = "success" ]; then - LABELS="${{ needs.apply-labels.outputs.labels-applied }}" - if [ "$LABELS" != "[]" ] && [ -n "$LABELS" ]; then - echo "| ๐Ÿท๏ธ Labels Applied | $LABELS |" >> $GITHUB_STEP_SUMMARY + { + echo "## ๐Ÿ“‹ Actions Taken" + echo "" + echo "| Action | Result |" + echo "|--------|--------|" + } >> $GITHUB_STEP_SUMMARY + + if [ "$APPLY_LABELS_OUTCOME" = "success" ]; then + if [ "$APPLY_LABELS_OUTPUT" != "[]" ] && [ -n "$APPLY_LABELS_OUTPUT" ]; then + echo "| ๐Ÿท๏ธ Labels Applied | $APPLY_LABELS_OUTPUT |" >> $GITHUB_STEP_SUMMARY else echo "| ๐Ÿท๏ธ Labels Applied | None needed |" >> $GITHUB_STEP_SUMMARY fi + elif [ "$APPLY_LABELS_OUTCOME" = "skipped" ]; then + echo "| ๐Ÿท๏ธ Labels Applied | Skipped (disabled) |" >> $GITHUB_STEP_SUMMARY fi - # Assignee - if [ "${{ needs.assign-assignee.result }}" = "success" ]; then - if [ "${{ needs.assign-assignee.outputs.assignee-added }}" = "true" ]; then + if [ "$ASSIGN_OUTCOME" = "success" ]; then + if [ "$ASSIGN_OUTPUT" = "true" ]; then echo "| ๐Ÿ‘ค Default Assignee | Added |" >> $GITHUB_STEP_SUMMARY 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 - if [ "${{ needs.welcome-contributor.result }}" = "success" ]; then - if [ "${{ needs.welcome-contributor.outputs.welcomed }}" = "true" ]; then - echo "| ๐Ÿ‘‹ Welcome Message | Posted |" >> $GITHUB_STEP_SUMMARY - fi + if [ "$WELCOME_OUTCOME" = "success" ] && [ "$WELCOME_OUTPUT" = "true" ]; then + echo "| ๐Ÿ‘‹ Welcome Message | Posted |" >> $GITHUB_STEP_SUMMARY fi - # Size label - if [ "${{ needs.analyze-size.result }}" = "success" ]; then - SIZE_LABEL="${{ needs.analyze-size.outputs.size-label }}" - TOTAL_CHANGES="${{ needs.analyze-size.outputs.total-changes }}" + if [ "$SIZE_OUTCOME" = "success" ]; then 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 + elif [ "$SIZE_OUTCOME" = "skipped" ]; then + echo "| ๐Ÿ“ Size Analysis | Skipped |" >> $GITHUB_STEP_SUMMARY fi - else - 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 + { + echo "## ๐Ÿงน Cleanup Actions" + echo "" + echo "| Action | Result |" + echo "|--------|--------|" + } >> $GITHUB_STEP_SUMMARY + + if [ "$CACHE_OUTCOME" = "success" ]; then + echo "| ๐Ÿงน Cache Cleanup | ${CACHES_CLEANED} cache(s) cleaned |" >> $GITHUB_STEP_SUMMARY fi - # Branch deletion if [ "$PR_MERGED" = "true" ]; then - if [ "${{ needs.delete-branch.result }}" = "success" ]; then - DELETED="${{ needs.delete-branch.outputs.branch-deleted }}" - if [ "$DELETED" = "true" ]; then + if [ "$DELETE_OUTCOME" = "success" ]; then + if [ "$BRANCH_DELETED" = "true" ]; then echo "| ๐ŸŒฟ Branch Deletion | Deleted |" >> $GITHUB_STEP_SUMMARY - elif [ "$DELETED" = "skip" ]; then + elif [ "$BRANCH_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 + elif [ "$DELETE_OUTCOME" = "skipped" ]; then + echo "| ๐ŸŒฟ Branch Deletion | Skipped |" >> $GITHUB_STEP_SUMMARY fi fi fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ”ง Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "### ๐Ÿ”ง Configuration" + echo "" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| Default Assignee | @${DEFAULT_ASSIGNEE} |" + echo "| Apply Size Labels | ${APPLY_SIZE_LABELS} |" + echo "| Apply Type Labels | ${APPLY_TYPE_LABELS} |" + echo "| Welcome First-timers | ${WELCOME_FIRST_TIME} |" + echo "" + echo "---" + echo "๐Ÿค– _Automated by GitHub Actions_" + } >> $GITHUB_STEP_SUMMARY + + # ==================================================================================== + # Fork PRs + # + # Runs when the PR head points at a different repository (external contributor). + # Performs only the operations that are safe and meaningful for forks: + # fork+triage labels, default assignee, security-aware welcome notice, cache cleanup. + # + # NOT performed for forks: + # - Type labels (require pre-merge code review to be meaningful) + # - PR size analysis (gated to same-repo by original design) + # - Branch deletion (fork branches live in the contributor's repo) + # + # Permissions are intentionally narrower than the same-repo job above: + # NO `contents: write` since there is no work that requires it. + # ==================================================================================== + pr-management-fork: + name: ๐Ÿ”ง PR Management (Fork) + if: github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: write # Required: Delete GitHub Actions caches for closed PRs + contents: read # Sparse checkout of base branch only โ€” no write needed + issues: write # Required: Create fork/triage labels lazily if missing + pull-requests: write # Required: Apply labels, assign reviewers, post comments + + steps: + # -------------------------------------------------------------------- + # SECURITY-CRITICAL CHECKOUT โ€” explicit base-ref + sparse + no fetch-depth + # + # Identical to the same-repo job above; the redundancy is intentional so + # the security-critical configuration sits next to the code that runs + # under elevated fork-PR conditions. + # -------------------------------------------------------------------- + # semgrep:ignore github-actions-dangerous-checkout + # codeql:ignore GH001 + # checkov:skip=CKV_GHA_3:Base branch checkout is intentional and safe + # sonarcloud:S7631 โ€” false positive: base-ref sparse checkout only (see NOSONAR below) + - name: ๐Ÿ“ฅ Checkout base repo (sparse) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 โ€” NOSONAR(S7631): base-ref sparse checkout only; PR head is never checked out or executed + with: + persist-credentials: false + ref: ${{ github.base_ref || github.ref }} + fetch-depth: 1 + sparse-checkout: | + .github/env + .github/actions/load-env + + - name: ๐ŸŒ Load environment variables + id: load-env + uses: ./.github/actions/load-env + + # -------------------------------------------------------------------- + # Extract fork-management configuration (single jq pass) + # -------------------------------------------------------------------- + - name: ๐Ÿ”ง Extract configuration + env: + ENV_JSON: ${{ steps.load-env.outputs.env-json }} + run: | + echo "๐Ÿ“‹ Extracting fork PR management configuration..." + + { + echo "DEFAULT_ASSIGNEE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE // ""')" + echo "SKIP_BOT_USERS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_SKIP_BOT_USERS // ""')" + echo "FORK_LABEL=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_FORK_LABEL // \"fork-pr\"')" + echo "TRIAGE_LABEL=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_TRIAGE_LABEL // \"requires-manual-review\"')" + echo "WELCOME_FORKS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FORKS // \"true\"')" + echo "CLEAN_CACHE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_CLEAN_CACHE_ON_CLOSE // \"true\"')" + } >> "$GITHUB_ENV" + + echo "๐Ÿ” Configuration loaded:" + echo " ๐Ÿ‘ค Default assignee: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE // ""')" + echo " ๐Ÿท๏ธ Fork label: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_FORK_LABEL // \"fork-pr\"')" + echo " ๐Ÿท๏ธ Triage label: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_TRIAGE_LABEL // \"requires-manual-review\"')" + echo " ๐Ÿ‘‹ Welcome forks: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FORKS // \"true\"')" + echo " ๐Ÿงน Clean cache on close: $(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_CLEAN_CACHE_ON_CLOSE // \"true\"')" + + # -------------------------------------------------------------------- + # Debug log: confirm fork classification (the job-level `if:` already + # enforces this, but logging the values is useful when triaging issues). + # -------------------------------------------------------------------- + - name: ๐Ÿ” Fork detection (debug) + 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 " Action: ${{ github.event.action }}" + echo "๐Ÿšจ FORK PR confirmed (job-level if would have skipped otherwise)" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + + # -------------------------------------------------------------------- + # Apply fork + triage labels (lazy-creates the labels if missing) + # -------------------------------------------------------------------- + - name: ๐Ÿท๏ธ Add fork + triage labels + id: fork-labels + if: github.event.action != 'closed' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const prNumber = pr.number; + 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 labels for bot user: ${author}`); + return; + } + + const ensureLabels = async (names) => { + // Create missing labels lazily with 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]); + + # -------------------------------------------------------------------- + # Assign default assignee if configured (skip when unset) + # -------------------------------------------------------------------- + - name: ๐Ÿ‘ค Assign default assignee (optional) + id: fork-assign + if: github.event.action != 'closed' && env.DEFAULT_ASSIGNEE != '' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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.'); + } + + # -------------------------------------------------------------------- + # Welcome notice for fork contributors (security-focused; explains why + # certain CI checks are restricted on fork PRs). + # -------------------------------------------------------------------- + - name: ๐Ÿ’ฌ Welcome fork contributor + id: fork-welcome + if: github.event.action == 'opened' && env.WELCOME_FORKS == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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. - # Extract key configuration for display - DEFAULT_ASSIGNEE=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_DEFAULT_ASSIGNEE') - APPLY_SIZE_LABELS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_SIZE_LABELS') - APPLY_TYPE_LABELS=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_APPLY_TYPE_LABELS') - WELCOME_FIRST_TIME=$(echo "$ENV_JSON" | jq -r '.PR_MANAGEMENT_WELCOME_FIRST_TIME') + Thanks for contributing to **${repoOwner}/${repoName}**! ๐Ÿš€ - echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY - echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Default Assignee | @$DEFAULT_ASSIGNEE |" >> $GITHUB_STEP_SUMMARY - echo "| Apply Size Labels | $APPLY_SIZE_LABELS |" >> $GITHUB_STEP_SUMMARY - echo "| Apply Type Labels | $APPLY_TYPE_LABELS |" >> $GITHUB_STEP_SUMMARY - echo "| Welcome First-timers | $WELCOME_FIRST_TIME |" >> $GITHUB_STEP_SUMMARY + `; - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿค– _Automated by GitHub Actions_" >> $GITHUB_STEP_SUMMARY + // Avoid duplicate welcome comments across re-opens / syncs + 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`); + } # -------------------------------------------------------------------- - # Report final workflow status + # Cache cleanup on PR close (mirrors the same-repo job โ€” fork PR + # caches live in the BASE repo's cache pool too). # -------------------------------------------------------------------- - - name: ๐Ÿ“ข Report workflow status + - name: ๐Ÿงน Cleanup caches + id: fork-clean-cache + if: github.event.action == 'closed' && env.CLEAN_CACHE == 'true' env: PR_NUMBER: ${{ github.event.pull_request.number }} - PR_ACTION: ${{ github.event.action }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_MERGED: ${{ github.event.pull_request.merged }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} run: | - echo "=== ๐Ÿ”ง Pull Request Management Summary ===" - echo "๐Ÿ“‹ PR: #$PR_NUMBER" - echo "๐ŸŽฌ Action: $PR_ACTION" - echo "๐Ÿ‘ค Author: @$PR_AUTHOR" + echo "๐Ÿงน Cleaning up caches for fork PR #$PR_NUMBER..." + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" - # Summary based on action - if [ "$PR_ACTION" != "closed" ]; then - echo "โœ… PR management tasks completed" + echo "๐Ÿ“‹ Fetching cache list for PR #$PR_NUMBER..." + allCaches=$(gh cache list --limit 100 --json id,key,ref) + + echo "๐Ÿ” Looking for caches with refs:" + echo " - refs/pull/$PR_NUMBER/merge" + echo " - refs/pull/$PR_NUMBER/head" + echo " - refs/heads/$PR_HEAD_REF" + + # PR_HEAD_REF is read from env (not interpolated into the jq filter) + # to prevent jq-injection via crafted branch names in fork PRs. + 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') + + if [ -z "$cacheKeysForPR" ]; then + cacheCount=0 else - if [ "$PR_MERGED" = "true" ]; then - echo "โœ… PR merged and cleanup completed" + 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" + + set +e + cleanedCount=0 + + for cacheKey in $cacheKeysForPR; do + if gh cache delete "$cacheKey"; then + echo " โœ… Deleted cache: $cacheKey" + ((cleanedCount++)) else - echo "โœ… PR closed and cleanup completed" + echo " โš ๏ธ Failed to delete cache: $cacheKey" fi + done + + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo "โœ… Cleaned $cleanedCount out of $cacheCount cache(s)" + echo "caches-cleaned=$cleanedCount" >> $GITHUB_OUTPUT + + # -------------------------------------------------------------------- + # Workflow summary + # -------------------------------------------------------------------- + - name: ๐Ÿ“Š Generate workflow summary + if: always() + 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 }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} + CACHE_OUTCOME: ${{ steps.fork-clean-cache.outcome }} + CACHES_CLEANED: ${{ steps.fork-clean-cache.outputs.caches-cleaned }} + run: | + { + echo "# ๐Ÿ”ง Pull Request Management Summary (Fork)" + echo "" + echo "**PR:** #$PR_NUMBER โ€” $PR_TITLE" + echo "**Author:** @$PR_AUTHOR" + echo "**Action:** $PR_ACTION" + echo "" + echo "## ๐Ÿ” Fork Detection" + echo "" + echo "| Property | Value |" + echo "|----------|-------|" + echo "| PR Head Repo | \`$PR_HEAD_REPO\` |" + echo "| Base Repo | \`$BASE_REPO\` |" + echo "| Is Fork PR? | **true** |" + echo "| Status | โœ… Fork PR โ€” handled with restricted permissions |" + echo "" + } >> $GITHUB_STEP_SUMMARY + + if [ "$PR_ACTION" = "closed" ]; then + { + echo "## ๐Ÿงน Cleanup Actions" + echo "" + echo "| Action | Result |" + echo "|--------|--------|" + } >> $GITHUB_STEP_SUMMARY + + if [ "$CACHE_OUTCOME" = "success" ]; then + echo "| ๐Ÿงน Cache Cleanup | ${CACHES_CLEANED} cache(s) cleaned |" >> $GITHUB_STEP_SUMMARY + elif [ "$CACHE_OUTCOME" = "skipped" ]; then + echo "| ๐Ÿงน Cache Cleanup | Skipped (disabled) |" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY fi - echo "๐Ÿ• Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" - echo "โœ… Workflow completed!" + { + echo "---" + echo "**Security:** This workflow used **pull_request_target** with **base-branch sparse checkout only**. PR code was **not** checked out or executed." + } >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3c7d4f0..417fddd 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -24,7 +24,8 @@ permissions: {} jobs: analysis: name: Scorecard analysis - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 10 # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: diff --git a/.github/workflows/stale-check.yml b/.github/workflows/stale-check.yml index c3d2f60..1934829 100644 --- a/.github/workflows/stale-check.yml +++ b/.github/workflows/stale-check.yml @@ -9,7 +9,7 @@ # centralized management across all workflows. # # Triggers: -# - Scheduled: Monday-Friday at 08:32 UTC +# - Scheduled: Monday-Friday at 12:00 UTC # - Manual: Via workflow_dispatch # # Maintainer: @mrz1836 @@ -39,15 +39,27 @@ concurrency: jobs: # ---------------------------------------------------------------------------------- - # Load Environment Variables + # Stale processing (single consolidated job) + # + # Workflow phases (each preserved as a step): + # ๐ŸŒ Load environment โ†’ reads .github/env/* + # ๐Ÿ”‘ Log token configuration โ†’ indicates which token will be used + # ๐Ÿ”ง Extract configuration โ†’ STALE_* vars into step outputs + # ๐Ÿ“… Calculate cutoff dates โ†’ stale/close window math + # ๐Ÿ“‹ Process stale issues โ†’ mark/close inactive issues + # ๐Ÿ”€ Process stale PRs โ†’ mark/close inactive PRs + # ๐Ÿท๏ธ Remove stale labels โ†’ clean labels off recently-updated items + # ๐Ÿ“Š Generate summary โ†’ step summary # ---------------------------------------------------------------------------------- - load-env: - name: ๐ŸŒ Load Environment Variables - runs-on: ubuntu-latest + stale-check: + name: ๐Ÿงน Process Stale Items + runs-on: ubuntu-24.04 + timeout-minutes: 10 permissions: - contents: read # Required: Read repository content for sparse checkout - outputs: - env-json: ${{ steps.load-env.outputs.env-json }} + contents: read # Required: Sparse checkout for env files + issues: write # Required to add labels and comments + pull-requests: write # Required to add labels and comments on PRs + steps: # -------------------------------------------------------------------- # Check out code to access env file @@ -55,6 +67,7 @@ jobs: - name: ๐Ÿ“ฅ Checkout code (sparse) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false sparse-checkout: | .github/env .github/actions/load-env @@ -63,27 +76,15 @@ jobs: # Load and parse environment file # -------------------------------------------------------------------- - name: ๐ŸŒ Load environment variables - uses: ./.github/actions/load-env id: load-env + uses: ./.github/actions/load-env - # ---------------------------------------------------------------------------------- - # Main Stale Check Job - # ---------------------------------------------------------------------------------- - stale-check: - name: ๐Ÿงน Process Stale Items - needs: [load-env] - runs-on: ubuntu-latest - permissions: - issues: write # Required to add labels and comments - pull-requests: write # Required to add labels and comments on PRs - - steps: # -------------------------------------------------------------------- # Log token configuration # -------------------------------------------------------------------- - name: ๐Ÿ”‘ Log token configuration env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} GH_PAT_TOKEN: ${{ secrets.GH_PAT_TOKEN }} run: | PREFERRED_TOKEN=$(echo "$ENV_JSON" | jq -r '.PREFERRED_GITHUB_TOKEN') @@ -100,7 +101,7 @@ jobs: - name: ๐Ÿ”ง Extract stale configuration id: config env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} run: | echo "๐ŸŽฏ Extracting stale workflow configuration..." @@ -113,12 +114,14 @@ jobs: OPERATIONS_PER_RUN=$(echo "$ENV_JSON" | jq -r '.STALE_OPERATIONS_PER_RUN') # Export to outputs - echo "days-before-stale=$DAYS_BEFORE_STALE" >> $GITHUB_OUTPUT - echo "days-before-close=$DAYS_BEFORE_CLOSE" >> $GITHUB_OUTPUT - echo "stale-label=$STALE_LABEL" >> $GITHUB_OUTPUT - echo "exempt-issue-labels=$EXEMPT_ISSUE_LABELS" >> $GITHUB_OUTPUT - echo "exempt-pr-labels=$EXEMPT_PR_LABELS" >> $GITHUB_OUTPUT - echo "operations-per-run=$OPERATIONS_PER_RUN" >> $GITHUB_OUTPUT + { + echo "days-before-stale=$DAYS_BEFORE_STALE" + echo "days-before-close=$DAYS_BEFORE_CLOSE" + echo "stale-label=$STALE_LABEL" + echo "exempt-issue-labels=$EXEMPT_ISSUE_LABELS" + echo "exempt-pr-labels=$EXEMPT_PR_LABELS" + echo "operations-per-run=$OPERATIONS_PER_RUN" + } >> $GITHUB_OUTPUT echo "โœ… Configuration extracted successfully" @@ -153,7 +156,7 @@ jobs: - name: ๐Ÿ“‹ Process stale issues uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} GH_PAT_TOKEN: ${{ secrets.GH_PAT_TOKEN }} with: github-token: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} @@ -291,7 +294,7 @@ jobs: - name: ๐Ÿ”€ Process stale pull requests uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} with: github-token: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -536,8 +539,9 @@ jobs: # Generate a workflow summary report # -------------------------------------------------------------------- - name: ๐Ÿ“Š Generate workflow summary + if: always() env: - ENV_JSON: ${{ needs.load-env.outputs.env-json }} + ENV_JSON: ${{ steps.load-env.outputs.env-json }} GH_PAT_TOKEN: ${{ secrets.GH_PAT_TOKEN }} run: | echo "๐Ÿš€ Generating workflow summary..." @@ -550,26 +554,25 @@ jobs: TOKEN_TYPE="๐Ÿ”‘ Default GITHUB_TOKEN" fi - echo "# ๐Ÿงน Stale Check Workflow Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**โฐ Completed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "## โš™๏ธ Configuration" >> $GITHUB_STEP_SUMMARY - echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY - echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Days before stale | ${{ steps.config.outputs.days-before-stale }} |" >> $GITHUB_STEP_SUMMARY - echo "| Days before close | ${{ steps.config.outputs.days-before-close }} |" >> $GITHUB_STEP_SUMMARY - echo "| Stale label | ${{ steps.config.outputs.stale-label }} |" >> $GITHUB_STEP_SUMMARY - echo "| Operations limit | ${{ steps.config.outputs.operations-per-run }} |" >> $GITHUB_STEP_SUMMARY - echo "| Token type | $TOKEN_TYPE |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "## ๐Ÿท๏ธ Exempt Labels" >> $GITHUB_STEP_SUMMARY - echo "- **Issues:** ${{ steps.config.outputs.exempt-issue-labels }}" >> $GITHUB_STEP_SUMMARY - echo "- **Pull Requests:** ${{ steps.config.outputs.exempt-pr-labels }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "๐Ÿ“‹ _Check the job logs above for detailed processing statistics._" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… **Stale check workflow completed successfully!**" >> $GITHUB_STEP_SUMMARY + { + echo "# ๐Ÿงน Stale Check Workflow Summary" + echo "" + echo "**โฐ Completed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "" + echo "## โš™๏ธ Configuration" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| Days before stale | ${{ steps.config.outputs.days-before-stale }} |" + echo "| Days before close | ${{ steps.config.outputs.days-before-close }} |" + echo "| Stale label | ${{ steps.config.outputs.stale-label }} |" + echo "| Operations limit | ${{ steps.config.outputs.operations-per-run }} |" + echo "| Token type | $TOKEN_TYPE |" + echo "" + echo "## ๐Ÿท๏ธ Exempt Labels" + echo "- **Issues:** ${{ steps.config.outputs.exempt-issue-labels }}" + echo "- **Pull Requests:** ${{ steps.config.outputs.exempt-pr-labels }}" + echo "" + echo "๐Ÿ“‹ _Check the job logs above for detailed processing statistics._" + echo "" + echo "โœ… **Stale check workflow completed successfully!**" + } >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index 366697a..bad0017 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -55,7 +55,8 @@ jobs: # ---------------------------------------------------------------------------------- load-env: name: ๐ŸŒ Load Environment Variables - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 5 permissions: contents: read # Required: Read repository content for sparse checkout outputs: @@ -68,6 +69,7 @@ jobs: - name: ๐Ÿ“ฅ Checkout code (sparse) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false sparse-checkout: | .github/env .github/actions/load-env @@ -106,7 +108,8 @@ jobs: sync-labels: name: ๐Ÿท๏ธ Sync Labels needs: [load-env] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 5 permissions: contents: read issues: write # Required for label management @@ -137,6 +140,7 @@ jobs: - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + persist-credentials: false fetch-depth: 2 # Fetch enough history to check parent commits # --------------------------------------------------------------------