From 08775ade2efcef87ac30008310806a35c5ce80a4 Mon Sep 17 00:00:00 2001 From: Ilan Lidovski Date: Sun, 12 Jul 2026 14:28:19 +0300 Subject: [PATCH] CM-68554: Build and sign Windows onedir executable Enable the windows-2022/onedir matrix entry, parametrize the signing and test steps for the onedir exe path, and sign only the unsigned PE files under _internal (vendor-signed binaries keep their original signatures). The onedir archive is named cycode-win-onedir.zip (no .exe infix). Co-Authored-By: Claude Fable 5 --- .github/workflows/build_executable.yml | 38 ++++++++++++++++++++++---- process_executable_file.py | 13 +++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_executable.yml b/.github/workflows/build_executable.yml index a9e5eddc..2b81f2cc 100644 --- a/.github/workflows/build_executable.yml +++ b/.github/workflows/build_executable.yml @@ -26,8 +26,6 @@ jobs: exclude: - os: ubuntu-22.04 mode: onedir - - os: windows-2022 - mode: onedir runs-on: ${{ matrix.os }} @@ -249,18 +247,46 @@ jobs: C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user smctl windows certsync --keypair-alias=%SM_KEYPAIR_ALIAS% - :: sign executable - signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ".\dist\cycode-cli.exe" + :: sign executable (in onedir mode the exe lives inside the collected directory) + set "EXE_PATH=.\dist\cycode-cli.exe" + if "${{ matrix.mode }}"=="onedir" set "EXE_PATH=.\dist\cycode-cli\cycode-cli.exe" + signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "%EXE_PATH%" + + - name: Sign unsigned onedir binaries (Windows) + if: runner.os == 'Windows' && matrix.mode == 'onedir' + shell: powershell + env: + SM_HOST: ${{ secrets.SM_HOST }} + SM_API_KEY: ${{ secrets.SM_API_KEY }} + SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} + run: | + # Vendor binaries (PSF-signed stdlib .pyds, python3xx.dll, Microsoft VC runtime) already + # carry valid signatures; re-signing would replace them with ours. Sign only the unsigned + # ones (PyInstaller-generated and third-party wheel binaries). + $files = Get-ChildItem -Path dist\cycode-cli\_internal -Recurse -Include *.dll,*.pyd,*.exe | + Where-Object { (Get-AuthenticodeSignature $_.FullName).Status -eq 'NotSigned' } + if (-not $files) { + Write-Host 'No unsigned binaries found' + exit 0 + } + Write-Host "Signing $($files.Count) unsigned binaries:" + $files.FullName | Write-Host + signtool.exe sign /sha1 $env:SM_CODE_SIGNING_CERT_SHA1_HASH /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 @($files.FullName) + exit $LASTEXITCODE - name: Test Windows signed executable if: runner.os == 'Windows' shell: cmd run: | + set "EXE_PATH=.\dist\cycode-cli.exe" + if "${{ matrix.mode }}"=="onedir" set "EXE_PATH=.\dist\cycode-cli\cycode-cli.exe" + :: call executable and expect correct output - .\dist\cycode-cli.exe status + "%EXE_PATH%" status :: verify signature - signtool.exe verify /v /pa ".\dist\cycode-cli.exe" + signtool.exe verify /v /pa "%EXE_PATH%" - name: Prepare files for artifact and release (rename and calculate sha256) run: echo "ARTIFACT_NAME=$(./process_executable_file.py dist/cycode-cli)" >> $GITHUB_ENV diff --git a/process_executable_file.py b/process_executable_file.py index 36d6d0d6..19cfbb44 100755 --- a/process_executable_file.py +++ b/process_executable_file.py @@ -22,7 +22,7 @@ _OS_TO_CLI_DIST_TEMPLATE = { 'darwin': Template('cycode-mac$suffix$ext'), 'linux': Template('cycode-linux$suffix$ext'), - 'windows': Template('cycode-win$suffix.exe$ext'), + 'windows': Template('cycode-win$suffix$ext'), } _WINDOWS = 'windows' _WINDOWS_EXECUTABLE_SUFFIX = '.exe' @@ -87,8 +87,7 @@ def get_cli_file_name(suffix: str = '', ext: str = '') -> str: if os_name not in _OS_TO_CLI_DIST_TEMPLATE: raise Exception(f'Unsupported OS: {os_name}') - template = _OS_TO_CLI_DIST_TEMPLATE[os_name] - return template.substitute(suffix=suffix, ext=ext) + return _OS_TO_CLI_DIST_TEMPLATE[os_name].substitute(suffix=suffix, ext=ext) def get_cli_file_suffix(is_onedir: bool) -> str: @@ -117,7 +116,9 @@ def write_hashes_db_to_file(hashes: DirHashes, output_path: str) -> None: def get_cli_filename(is_onedir: bool) -> str: - return get_cli_file_name(get_cli_file_suffix(is_onedir)) + # onedir is distributed as an archive of a directory, so only onefile carries .exe + ext = _WINDOWS_EXECUTABLE_SUFFIX if get_os_name() == _WINDOWS and not is_onedir else '' + return get_cli_file_name(suffix=get_cli_file_suffix(is_onedir), ext=ext) def get_cli_path(output_path: Path, is_onedir: bool) -> str: @@ -125,7 +126,7 @@ def get_cli_path(output_path: Path, is_onedir: bool) -> str: def get_cli_hash_filename(is_onedir: bool) -> str: - return get_cli_file_name(suffix=get_cli_file_suffix(is_onedir), ext=_HASH_FILE_EXT) + return get_cli_filename(is_onedir) + _HASH_FILE_EXT def get_cli_hash_path(output_path: Path, is_onedir: bool) -> str: @@ -133,7 +134,7 @@ def get_cli_hash_path(output_path: Path, is_onedir: bool) -> str: def get_cli_archive_filename(is_onedir: bool) -> str: - return get_cli_file_name(suffix=get_cli_file_suffix(is_onedir)) + return get_cli_filename(is_onedir) def get_cli_archive_path(output_path: Path, is_onedir: bool) -> str: