@@ -132,15 +132,34 @@ jobs:
132132 env :
133133 APPLE_CERT_NAME : ${{ secrets.APPLE_CERT_NAME }}
134134 run : |
135- # Sign all Mach-O binaries in the onedir output (excluding the main executable)
136- # Main executable must be signed last after all its dependencies
137- find dist/cycode-cli -type f ! -name "cycode-cli" | while read -r file; do
135+ # 1. Sign framework bundles (preserves Info.plist binding for --strict verification)
136+ while IFS= read -r framework; do
137+ echo "Signing framework: $framework"
138+ codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime "$framework"
139+ done < <(find dist/cycode-cli -name "*.framework" -type d -maxdepth 3)
140+
141+ # 2. Replace standalone Python binary with symlink to the framework version.
142+ # The standalone copy fails codesign --verify --strict because it lacks the
143+ # framework's Info.plist context. The framework version passes.
144+ if [ -f dist/cycode-cli/_internal/Python ] && [ -d dist/cycode-cli/_internal/Python.framework ]; then
145+ FRAMEWORK_PYTHON=$(find dist/cycode-cli/_internal/Python.framework/Versions -name "Python" -type f | head -1)
146+ if [ -n "$FRAMEWORK_PYTHON" ]; then
147+ RELATIVE_PATH=${FRAMEWORK_PYTHON#dist/cycode-cli/_internal/}
148+ echo "Replacing _internal/Python with symlink to $RELATIVE_PATH"
149+ rm dist/cycode-cli/_internal/Python
150+ ln -s "$RELATIVE_PATH" dist/cycode-cli/_internal/Python
151+ fi
152+ fi
153+
154+ # 3. Sign remaining Mach-O files (dylibs, .so files) outside of framework bundles
155+ find dist/cycode-cli -type f ! -name "cycode-cli" ! -path "*.framework/*" | while read -r file; do
138156 if file -b "$file" | grep -q "Mach-O"; then
157+ echo "Signing: $file"
139158 codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime "$file"
140159 fi
141160 done
142161
143- # Re-sign the main executable with entitlements (must be last)
162+ # 4. Re-sign the main executable with entitlements (must be last)
144163 codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime --entitlements entitlements.plist dist/cycode-cli/cycode-cli
145164
146165 - name : Notarize macOS executable
@@ -176,15 +195,37 @@ jobs:
176195
177196 # we can't staple the app because it's executable
178197
198+ - name : Verify macOS code signatures
199+ if : runner.os == 'macOS'
200+ run : |
201+ # verify all Mach-O binaries are properly signed
202+ # use -L to follow symlinks (e.g. _internal/Python -> Python.framework/...)
203+ FAILED=false
204+ while IFS= read -r file; do
205+ if file -b "$file" | grep -q "Mach-O"; then
206+ if ! codesign --verify --strict "$file" 2>&1; then
207+ echo "INVALID: $file"
208+ codesign -dv "$file" 2>&1 || true
209+ FAILED=true
210+ else
211+ echo "OK: $file"
212+ fi
213+ fi
214+ done < <(find -L dist/cycode-cli -type f)
215+
216+ if [ "$FAILED" = true ]; then
217+ echo "Found binaries with invalid signatures!"
218+ exit 1
219+ fi
220+
221+ codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE
222+
179223 - name : Test macOS signed executable
180224 if : runner.os == 'macOS'
181225 run : |
182226 file -b $PATH_TO_CYCODE_CLI_EXECUTABLE
183227 time $PATH_TO_CYCODE_CLI_EXECUTABLE version
184228
185- # verify signature
186- codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE
187-
188229 - name : Import cert for Windows and setup envs
189230 if : runner.os == 'Windows'
190231 env :
@@ -236,6 +277,46 @@ jobs:
236277 name : ${{ env.ARTIFACT_NAME }}
237278 path : dist
238279
280+ - name : Verify macOS artifact end-to-end
281+ if : runner.os == 'macOS' && matrix.mode == 'onedir'
282+ uses : actions/download-artifact@v4
283+ with :
284+ name : ${{ env.ARTIFACT_NAME }}
285+ path : /tmp/artifact-verify
286+
287+ - name : Verify macOS artifact signatures and run with quarantine
288+ if : runner.os == 'macOS' && matrix.mode == 'onedir'
289+ run : |
290+ # extract the onedir zip exactly as an end user would
291+ ARCHIVE=$(find /tmp/artifact-verify -name "*.zip" | head -1)
292+ echo "Verifying archive: $ARCHIVE"
293+ unzip "$ARCHIVE" -d /tmp/artifact-extracted
294+
295+ # verify all Mach-O code signatures (strict mode)
296+ FAILED=false
297+ while IFS= read -r file; do
298+ if file -b "$file" | grep -q "Mach-O"; then
299+ if ! codesign --verify --strict "$file" 2>&1; then
300+ echo "INVALID: $file"
301+ codesign -dv "$file" 2>&1 || true
302+ FAILED=true
303+ else
304+ echo "OK: $file"
305+ fi
306+ fi
307+ done < <(find -L /tmp/artifact-extracted -type f)
308+
309+ if [ "$FAILED" = true ]; then
310+ echo "Artifact contains binaries with invalid signatures!"
311+ exit 1
312+ fi
313+
314+ # simulate download quarantine and test execution
315+ find -L /tmp/artifact-extracted -type f -exec xattr -w com.apple.quarantine "0081;$(printf '%x' $(date +%s));CI;$(uuidgen)" {} \;
316+ EXECUTABLE=$(find -L /tmp/artifact-extracted -name "cycode-cli" -type f | head -1)
317+ echo "Testing quarantined executable: $EXECUTABLE"
318+ time "$EXECUTABLE" version
319+
239320 - name : Upload files to release
240321 if : ${{ github.event_name == 'workflow_dispatch' && inputs.publish }}
241322 uses : svenstaro/upload-release-action@v2
0 commit comments