Refactor: Use safe type assertion helper for closures in topology analysis #74
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Semantic Analysis | |
| on: | |
| push: | |
| branches: [ "main" ] | |
| pull_request: | |
| branches: [ "main" ] | |
| permissions: | |
| contents: read | |
| jobs: | |
| analysis: | |
| name: Run Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.24' | |
| cache: true | |
| - name: Build SFW | |
| # Building strictly from source to ensure binary integrity before execution. | |
| run: | | |
| mkdir -p bin | |
| CGO_ENABLED=0 go build -o bin/sfw ./cmd/sfw | |
| chmod +x bin/sfw | |
| # Verify binary works | |
| ./bin/sfw --version || ./bin/sfw help || true | |
| - name: Determine Mode | |
| id: mode | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| echo "mode=BLOCKER" >> $GITHUB_OUTPUT | |
| else | |
| echo "mode=check" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Prepare Analysis Environment | |
| id: prep | |
| run: | | |
| # 1. Determine Refs | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| PR_BASE="${{ github.event.pull_request.base.ref }}" | |
| if [[ -n "$PR_BASE" ]]; then | |
| BASE_REF="origin/$PR_BASE" | |
| else | |
| # Fallback for local testing with act (PR context not fully populated) | |
| echo "::warning::PR base ref not available. Falling back to HEAD~1." | |
| BASE_REF="HEAD~1" | |
| fi | |
| HEAD_REF="HEAD" | |
| else | |
| git fetch origin main --depth=100 2>/dev/null || true | |
| BASE_REF=$(git merge-base origin/main HEAD 2>/dev/null || echo "HEAD~1") | |
| HEAD_REF="HEAD" | |
| fi | |
| echo "::notice::Base ref: $BASE_REF, Head ref: $HEAD_REF" | |
| # 2. Setup Base Worktree | |
| # We create the worktree in the workspace so it is mounted into the sandbox. | |
| WORKTREE_DIR=".sfw_base" | |
| # Clean up previous run if exists | |
| rm -rf "$WORKTREE_DIR" | |
| git worktree add --detach "$WORKTREE_DIR" "$BASE_REF" | |
| echo "worktree_dir=$WORKTREE_DIR" >> $GITHUB_OUTPUT | |
| # 3. Pre-calculate Diff (CRITICAL HARDENING) | |
| # We run git on the host (where it works) and save the status to a file. | |
| # The sandbox will simply read this file, avoiding "dubious ownership" errors. | |
| git diff -z --name-status "$BASE_REF" "$HEAD_REF" > diff_stream.bin | |
| # Check for Go files to fail fast if none exist | |
| if git diff --name-only "$BASE_REF" "$HEAD_REF" | grep -q '\.go$'; then | |
| echo "has_go_files=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_go_files=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Run Semantic Analysis (Sandboxed) | |
| uses: geomys/sandboxed-step@v1.2.0 | |
| with: | |
| disable-network: 'true' | |
| # CONTEXT INJECTION: | |
| # We inject vars via interpolation. We write to a local artifact | |
| # instead of trying to hit the runner's step summary directly. | |
| run: | | |
| export MODE="${{ steps.mode.outputs.mode }}" | |
| export WORKTREE_DIR="${{ steps.prep.outputs.worktree_dir }}" | |
| export HAS_GO="${{ steps.prep.outputs.has_go_files }}" | |
| export SFW_SANDBOX_ID="1" | |
| # Strict mode enabled after exports | |
| set -euo pipefail | |
| # Define local report artifact (Safe Write) | |
| REPORT_FILE="scan_report.md" | |
| echo "## Semantic Analysis Report ($MODE)" >> "$REPORT_FILE" | |
| echo "| File | Status | Match % |" >> "$REPORT_FILE" | |
| echo "| :--- | :--- | :--- |" >> "$REPORT_FILE" | |
| if [ "$HAS_GO" != "true" ]; then | |
| echo "No Go files changed." >> "$REPORT_FILE" | |
| exit 0 | |
| fi | |
| # Check for required tools inside sandbox | |
| if ! command -v jq >/dev/null; then | |
| echo "::error::'jq' is missing from the sandbox environment." | |
| exit 1 | |
| fi | |
| ERROR_COUNT=0 | |
| LOGIC_FAIL=0 | |
| # Process the pre-calculated diff stream | |
| while IFS= read -r -d '' status; do | |
| case "$status" in | |
| R*|C*) | |
| IFS= read -r -d '' old_path | |
| IFS= read -r -d '' new_path | |
| OLD_FILE_REF="$old_path" | |
| NEW_FILE_REF="$new_path" | |
| ;; | |
| *) | |
| IFS= read -r -d '' path | |
| OLD_FILE_REF="$path" | |
| NEW_FILE_REF="$path" | |
| ;; | |
| esac | |
| if [[ "$NEW_FILE_REF" != *.go ]] && [[ "$OLD_FILE_REF" != *.go ]]; then continue; fi | |
| if [ -f "$NEW_FILE_REF" ]; then | |
| NEW_FILE="$NEW_FILE_REF" | |
| else | |
| NEW_FILE="" | |
| fi | |
| OLD_FILE="$WORKTREE_DIR/$OLD_FILE_REF" | |
| if [ ! -f "$OLD_FILE" ]; then | |
| OLD_FILE="" | |
| fi | |
| if [[ -z "$NEW_FILE" ]] && [[ -z "$OLD_FILE" ]]; then continue; fi | |
| if [[ -z "$OLD_FILE" ]]; then | |
| echo "| \`$NEW_FILE_REF\` | New File | N/A |" >> "$REPORT_FILE" | |
| continue | |
| fi | |
| if [[ -z "$NEW_FILE" ]]; then | |
| echo "| \`$OLD_FILE_REF\` | Deleted | N/A |" >> "$REPORT_FILE" | |
| continue | |
| fi | |
| # Execute SFW with stderr capture | |
| if ! OUTPUT=$(./bin/sfw diff "$OLD_FILE" "$NEW_FILE" 2>&1); then | |
| echo "::error::sfw failed to process $NEW_FILE_REF" | |
| ERROR_COUNT=$((ERROR_COUNT + 1)) | |
| continue | |
| fi | |
| # Validate JSON | |
| if ! echo "$OUTPUT" | jq -e . >/dev/null 2>&1; then | |
| echo "::error::Invalid JSON output for $NEW_FILE_REF" | |
| ERROR_COUNT=$((ERROR_COUNT + 1)) | |
| continue | |
| fi | |
| PCT=$(echo "$OUTPUT" | jq -r '.summary.semantic_match_pct // 0') | |
| MODIFIED=$(echo "$OUTPUT" | jq -r '.summary.modified // 0') | |
| IS_BELOW_100=$(echo "$OUTPUT" | jq -r 'if (.summary.semantic_match_pct // 0) < 100 then "true" else "false" end') | |
| if [ "$IS_BELOW_100" = "true" ]; then | |
| STATUS_ICON="Modified ($MODIFIED)" | |
| echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE" | |
| if [ "$MODE" == "BLOCKER" ]; then | |
| echo "::error file=$NEW_FILE_REF::Logic change detected in safe refactor! ($PCT%)" | |
| LOGIC_FAIL=1 | |
| fi | |
| else | |
| STATUS_ICON="Preserved" | |
| echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE" | |
| fi | |
| done < diff_stream.bin | |
| if [ $ERROR_COUNT -gt 0 ]; then | |
| echo "" >> "$REPORT_FILE" | |
| echo "**CI FAILED**: Tool execution failures detected." >> "$REPORT_FILE" | |
| exit 1 | |
| fi | |
| if [ $LOGIC_FAIL -eq 1 ]; then | |
| echo "" >> "$REPORT_FILE" | |
| echo "**CI FAILED**: Logic changed in 'semantic-safe' PR." >> "$REPORT_FILE" | |
| exit 1 | |
| fi | |
| - name: Publish Analysis Report | |
| if: always() | |
| # This runs on the host, so it has access to GITHUB_STEP_SUMMARY. | |
| # It reads the artifact produced by the sandbox. | |
| run: | | |
| if [ -f "scan_report.md" ]; then | |
| cat scan_report.md >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Cleanup Worktree | |
| if: always() | |
| env: | |
| WORKTREE_DIR: ${{ steps.prep.outputs.worktree_dir }} | |
| run: | | |
| if [ -d "$WORKTREE_DIR" ]; then | |
| git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || rm -rf "$WORKTREE_DIR" | |
| fi |