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
# --------------------------------------------------------------------