3636 # Verify binary works
3737 ./bin/sfw --version || ./bin/sfw help || true
3838
39+ - name : Vendor Dependencies
40+ # Vendor modules into workspace so they're available inside the sandbox.
41+ # The sandbox only mounts the workspace, not GOMODCACHE (/home/runner/go/pkg/mod).
42+ run : go mod vendor
43+
3944 - name : Determine Mode
4045 id : mode
4146 run : |
@@ -88,129 +93,123 @@ jobs:
8893 echo "has_go_files=false" >> $GITHUB_OUTPUT
8994 fi
9095
91- - name : Run Semantic Analysis (Sandboxed)
92- uses : geomys/sandboxed-step@v1.2.0
93- with :
94- disable-network : ' true'
95- # CONTEXT INJECTION:
96- # We inject vars via interpolation. We write to a local artifact
97- # instead of trying to hit the runner's step summary directly.
98- run : |
99- export MODE="${{ steps.mode.outputs.mode }}"
100- export WORKTREE_DIR="${{ steps.prep.outputs.worktree_dir }}"
101- export HAS_GO="${{ steps.prep.outputs.has_go_files }}"
102- export SFW_SANDBOX_ID="1"
103-
104- # Strict mode enabled after exports
105- set -euo pipefail
106-
107- # Define local report artifact (Safe Write)
108- REPORT_FILE="scan_report.md"
109-
110- echo "## Semantic Analysis Report ($MODE)" >> "$REPORT_FILE"
111- echo "| File | Status | Match % |" >> "$REPORT_FILE"
112- echo "| :--- | :--- | :--- |" >> "$REPORT_FILE"
113-
114- if [ "$HAS_GO" != "true" ]; then
115- echo "No Go files changed." >> "$REPORT_FILE"
116- exit 0
96+ # Install dependencies for sfw's internal sandbox (AppArmor bypass via aa-exec)
97+ - name : Install Sandbox Dependencies
98+ run : |
99+ sudo apt-get update
100+ sudo apt-get install -y apparmor-utils
101+
102+ - name : Run Semantic Analysis
103+ env :
104+ MODE : ${{ steps.mode.outputs.mode }}
105+ WORKTREE_DIR : ${{ steps.prep.outputs.worktree_dir }}
106+ HAS_GO : ${{ steps.prep.outputs.has_go_files }}
107+ # Use vendored dependencies - sandbox can't access GOMODCACHE
108+ GOFLAGS : " -mod=vendor"
109+ GOPROXY : " off"
110+ run : |
111+ set -euo pipefail
112+
113+ REPORT_FILE="scan_report.md"
114+
115+ echo "## Semantic Analysis Report ($MODE)" >> "$REPORT_FILE"
116+ echo "| File | Status | Match % |" >> "$REPORT_FILE"
117+ echo "| :--- | :--- | :--- |" >> "$REPORT_FILE"
118+
119+ if [ "$HAS_GO" != "true" ]; then
120+ echo "No Go files changed." >> "$REPORT_FILE"
121+ exit 0
122+ fi
123+
124+ ERROR_COUNT=0
125+ LOGIC_FAIL=0
126+
127+ # Process the pre-calculated diff stream
128+ while IFS= read -r -d '' status; do
129+ case "$status" in
130+ R*|C*)
131+ IFS= read -r -d '' old_path
132+ IFS= read -r -d '' new_path
133+ OLD_FILE_REF="$old_path"
134+ NEW_FILE_REF="$new_path"
135+ ;;
136+ *)
137+ IFS= read -r -d '' path
138+ OLD_FILE_REF="$path"
139+ NEW_FILE_REF="$path"
140+ ;;
141+ esac
142+
143+ if [[ "$NEW_FILE_REF" != *.go ]] && [[ "$OLD_FILE_REF" != *.go ]]; then continue; fi
144+
145+ if [ -f "$NEW_FILE_REF" ]; then
146+ NEW_FILE="$NEW_FILE_REF"
147+ else
148+ NEW_FILE=""
117149 fi
118150
119- # Check for required tools inside sandbox
120- if ! command -v jq >/dev/null; then
121- echo "::error::'jq' is missing from the sandbox environment."
122- exit 1
151+ OLD_FILE="$WORKTREE_DIR/$OLD_FILE_REF"
152+ if [ ! -f "$OLD_FILE" ]; then
153+ OLD_FILE=""
123154 fi
124155
125- ERROR_COUNT=0
126- LOGIC_FAIL=0
127-
128- # Process the pre-calculated diff stream
129- while IFS= read -r -d '' status; do
130- case "$status" in
131- R*|C*)
132- IFS= read -r -d '' old_path
133- IFS= read -r -d '' new_path
134- OLD_FILE_REF="$old_path"
135- NEW_FILE_REF="$new_path"
136- ;;
137- *)
138- IFS= read -r -d '' path
139- OLD_FILE_REF="$path"
140- NEW_FILE_REF="$path"
141- ;;
142- esac
143-
144- if [[ "$NEW_FILE_REF" != *.go ]] && [[ "$OLD_FILE_REF" != *.go ]]; then continue; fi
145-
146- if [ -f "$NEW_FILE_REF" ]; then
147- NEW_FILE="$NEW_FILE_REF"
148- else
149- NEW_FILE=""
150- fi
151-
152- OLD_FILE="$WORKTREE_DIR/$OLD_FILE_REF"
153- if [ ! -f "$OLD_FILE" ]; then
154- OLD_FILE=""
155- fi
156-
157- if [[ -z "$NEW_FILE" ]] && [[ -z "$OLD_FILE" ]]; then continue; fi
158-
159- if [[ -z "$OLD_FILE" ]]; then
160- echo "| \`$NEW_FILE_REF\` | New File | N/A |" >> "$REPORT_FILE"
161- continue
162- fi
163-
164- if [[ -z "$NEW_FILE" ]]; then
165- echo "| \`$OLD_FILE_REF\` | Deleted | N/A |" >> "$REPORT_FILE"
166- continue
167- fi
168-
169- # Execute SFW with stderr capture
170- if ! OUTPUT=$(./bin/sfw diff "$OLD_FILE" "$NEW_FILE" 2>&1); then
171- echo "::error::sfw failed to process $NEW_FILE_REF"
172- ERROR_COUNT=$((ERROR_COUNT + 1))
173- continue
174- fi
175-
176- # Validate JSON
177- if ! echo "$OUTPUT" | jq -e . >/dev/null 2>&1; then
178- echo "::error::Invalid JSON output for $NEW_FILE_REF"
179- ERROR_COUNT=$((ERROR_COUNT + 1))
180- continue
181- fi
182-
183- PCT=$(echo "$OUTPUT" | jq -r '.summary.semantic_match_pct // 0')
184- MODIFIED=$(echo "$OUTPUT" | jq -r '.summary.modified // 0')
185- IS_BELOW_100=$(echo "$OUTPUT" | jq -r 'if (.summary.semantic_match_pct // 0) < 100 then "true" else "false" end')
186-
187- if [ "$IS_BELOW_100" = "true" ]; then
188- STATUS_ICON="Modified ($MODIFIED)"
189- echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
190-
191- if [ "$MODE" == "BLOCKER" ]; then
192- echo "::error file=$NEW_FILE_REF::Logic change detected in safe refactor! ($PCT%)"
193- LOGIC_FAIL=1
194- fi
195- else
196- STATUS_ICON="Preserved"
197- echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
198- fi
199-
200- done < diff_stream.bin
201-
202- if [ $ERROR_COUNT -gt 0 ]; then
203- echo "" >> "$REPORT_FILE"
204- echo "**CI FAILED**: Tool execution failures detected." >> "$REPORT_FILE"
205- exit 1
156+ if [[ -z "$NEW_FILE" ]] && [[ -z "$OLD_FILE" ]]; then continue; fi
157+
158+ if [[ -z "$OLD_FILE" ]]; then
159+ echo "| \`$NEW_FILE_REF\` | New File | N/A |" >> "$REPORT_FILE"
160+ continue
206161 fi
207162
208- if [ $LOGIC_FAIL -eq 1 ]; then
209- echo "" >> "$REPORT_FILE"
210- echo "**CI FAILED**: Logic changed in 'semantic-safe' PR." >> "$REPORT_FILE"
211- exit 1
163+ if [[ -z "$NEW_FILE" ]]; then
164+ echo "| \`$OLD_FILE_REF\` | Deleted | N/A |" >> "$REPORT_FILE"
165+ continue
212166 fi
213167
168+ # Execute SFW diff (sfw handles its own sandboxing internally)
169+ if ! OUTPUT=$(./bin/sfw diff "$OLD_FILE" "$NEW_FILE" 2>&1); then
170+ echo "::error::sfw failed to process $NEW_FILE_REF"
171+ ERROR_COUNT=$((ERROR_COUNT + 1))
172+ continue
173+ fi
174+
175+ # Validate JSON
176+ if ! echo "$OUTPUT" | jq -e . >/dev/null 2>&1; then
177+ echo "::error::Invalid JSON output for $NEW_FILE_REF"
178+ ERROR_COUNT=$((ERROR_COUNT + 1))
179+ continue
180+ fi
181+
182+ PCT=$(echo "$OUTPUT" | jq -r '.summary.semantic_match_pct // 0')
183+ MODIFIED=$(echo "$OUTPUT" | jq -r '.summary.modified // 0')
184+ IS_BELOW_100=$(echo "$OUTPUT" | jq -r 'if (.summary.semantic_match_pct // 0) < 100 then "true" else "false" end')
185+
186+ if [ "$IS_BELOW_100" = "true" ]; then
187+ STATUS_ICON="Modified ($MODIFIED)"
188+ echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
189+
190+ if [ "$MODE" == "BLOCKER" ]; then
191+ echo "::error file=$NEW_FILE_REF::Logic change detected in safe refactor! ($PCT%)"
192+ LOGIC_FAIL=1
193+ fi
194+ else
195+ STATUS_ICON="Preserved"
196+ echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
197+ fi
198+
199+ done < diff_stream.bin
200+
201+ if [ $ERROR_COUNT -gt 0 ]; then
202+ echo "" >> "$REPORT_FILE"
203+ echo "**CI FAILED**: Tool execution failures detected." >> "$REPORT_FILE"
204+ exit 1
205+ fi
206+
207+ if [ $LOGIC_FAIL -eq 1 ]; then
208+ echo "" >> "$REPORT_FILE"
209+ echo "**CI FAILED**: Logic changed in 'semantic-safe' PR." >> "$REPORT_FILE"
210+ exit 1
211+ fi
212+
214213 - name : Publish Analysis Report
215214 if : always()
216215 # This runs on the host, so it has access to GITHUB_STEP_SUMMARY.
@@ -227,4 +226,4 @@ jobs:
227226 run : |
228227 if [ -d "$WORKTREE_DIR" ]; then
229228 git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || rm -rf "$WORKTREE_DIR"
230- fi
229+ fi
0 commit comments