From 81f5ab569113856477d95b44d86a1e13a5396299 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 22 Feb 2026 20:33:00 -0600 Subject: [PATCH] Packaging: Add Azure-based artifact signing on Win Sign all *.exe, *.dll, and *.pyd files in the bin directory of the build, as well as the two Chocolatey stubs and the installer. Requires the use of several environment variables and secrets, documented at https://github.com/FreeCAD/DevelopersHandbook/blob/main/technical/codesigning.md#windows --- .github/workflows/build_release.yml | 114 ++++++++++++------ package/rattler-build/osx/create_bundle.sh | 4 +- .../rattler-build/windows/create_bundle.sh | 84 +++++++++++++ 3 files changed, 163 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index e56dcc5f3e6a..a49ed0e13f78 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -10,6 +10,10 @@ permissions: contents: write actions: write +concurrency: + group: build-release-${{ github.event_name }} + cancel-in-progress: true + jobs: upload_src: runs-on: ubuntu-latest @@ -21,7 +25,7 @@ jobs: with: egress-policy: audit - - name: Checkout Source + - name: Checkout source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} @@ -29,7 +33,7 @@ jobs: fetch-tags: true submodules: 'recursive' - - name: get tag and create release if weekly + - name: Get tag and create release if weekly id: get_tag shell: bash -l {0} env: @@ -39,36 +43,15 @@ jobs: export BUILD_TAG="${{ github.event.release.tag_name }}" else export BUILD_TAG=weekly-$(date "+%Y.%m.%d") - gh release create ${BUILD_TAG} --title "Development Build ${BUILD_TAG}" -F .github/workflows/weekly-build-notes.md --prerelease || true + gh release create ${BUILD_TAG} --title "Development Build ${BUILD_TAG}" \ + -F .github/workflows/weekly-build-notes.md \ + --prerelease || \ + gh release view ${BUILD_TAG} > /dev/null # fail if it doesn't exist fi echo "BUILD_TAG=${BUILD_TAG}" >> "$GITHUB_ENV" echo "build_tag=${BUILD_TAG}" >> "$GITHUB_OUTPUT" - - name: Trigger notes updater workflow (only for weekly) - if: startsWith(steps.get_tag.outputs.build_tag, 'weekly-') - uses: actions/github-script@v7 - env: - WEEKLY_TAG: ${{ steps.get_tag.outputs.build_tag }} - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - - // Reusable/dispatchable updater workflow file in .github/workflows/ - const workflow_id = 'weekly-compare-link.yml'; - - // Use the default branch so the workflow file is available - const ref = (context.payload?.repository?.default_branch) || 'main'; - const current_tag = process.env.WEEKLY_TAG || ''; - - await github.rest.actions.createWorkflowDispatch({ - owner, repo, workflow_id, ref, - inputs: { current_tag } - }); - - core.info(`Dispatched ${workflow_id} on ${ref} with current_tag='${current_tag}'.`) - - - name: Upload Source + - name: Upload source id: upload_source shell: bash -l {0} env: @@ -118,7 +101,7 @@ jobs: remove-android: 'true' # (frees ~9 GB) remove-cached-tools: 'true' # (frees ~8.3 GB) - - name: Set Platform Environment Variables + - name: Set platform environment variables shell: bash -l {0} env: OPERATING_SYSTEM: ${{ runner.os }} @@ -128,7 +111,7 @@ jobs: echo 'RATTLER_CACHE_DIR=D:\rattler' >> "$GITHUB_ENV" fi - - name: Checkout Source + - name: Checkout source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} @@ -142,7 +125,7 @@ jobs: cache: false - name: Install the Apple certificate and provisioning profile - id: get_cert + id: macos_get_cert if: runner.os == 'macOS' env: APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }} @@ -150,7 +133,6 @@ jobs: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} DEVELOPER_TEAM_ID: ${{ secrets.DEVELOPER_TEAM_ID }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} run: | if [ -z "$BUILD_CERTIFICATE_BASE64" ]; then @@ -186,23 +168,81 @@ jobs: xcrun notarytool store-credentials "FreeCAD" --keychain "$KEYCHAIN_PATH" --apple-id "${APPLE_ID}" --password "${APP_SPECIFIC_PASSWORD}" --team-id "${DEVELOPER_TEAM_ID}" - - name: Build and Release Packages + - name: Setup .NET 10 SDK + if: runner.os == 'Windows' + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + + - name: Install sign tool + if: runner.os == 'Windows' + run: | + dotnet tool install --global --prerelease sign + echo "$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + dotnet --info + sign --version + + - name: Build packages shell: bash env: GH_TOKEN: ${{ github.token }} - SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} - SIGN_RELEASE: ${{ steps.get_cert.outputs.has_cert }} TARGET_PLATFORM: ${{ matrix.target }} - MAKE_INSTALLER: "true" - UPLOAD_RELEASE: "true" BUILD_TAG: ${{ needs.upload_src.outputs.build_tag }} run: | + set -euo pipefail if [[ "${{ runner.os }}" == "macOS" ]]; then export MACOS_DEPLOYMENT_TARGET="${{ matrix.deploy_target }}" fi python3 package/scripts/write_version_info.py ../freecad_version.txt cd package/rattler-build pixi install + + - name: Azure login for Windows build code signing + id: azure_login + if: runner.os == 'Windows' + continue-on-error: true + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 + with: + creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + + - name: Release packages with optional code-signing on Windows and macOS + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TARGET_PLATFORM: ${{ matrix.target }} + MAKE_INSTALLER: "true" + UPLOAD_RELEASE: "true" + BUILD_TAG: ${{ needs.upload_src.outputs.build_tag }} + run: | + set -euo pipefail + + export MACOS_SIGN_RELEASE=0 + export WINDOWS_SIGN_RELEASE=0 + + case "${RUNNER_OS}" in + Windows) + export WINDOWS_SIGN_RELEASE="${{ steps.azure_login.outcome }}" + export WINDOWS_AZURE_ENDPOINT="${{ vars.AZURE_TRUSTED_SIGNING_ENDPOINT }}" + export WINDOWS_AZURE_CERTIFICATE_PROFILE="${{ vars.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}" + export WINDOWS_AZURE_SIGNING_ACCOUNT="${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT }}" + + # For good measure, normalize the azure_login result to 1 or 0 + if [[ "${WINDOWS_SIGN_RELEASE:-}" == "success" ]]; then + export WINDOWS_SIGN_RELEASE=1 + else + export WINDOWS_SIGN_RELEASE=0 + fi + ;; + + macOS) + export MACOS_SIGN_RELEASE="${{ steps.macos_get_cert.outputs.has_cert }}" + export MACOS_SIGNING_KEY_ID="${{ secrets.SIGNING_KEY_ID }}" + export MACOS_DEPLOYMENT_TARGET="${{ matrix.deploy_target }}" + ;; + esac + + cd package/rattler-build pixi run -e package create_bundle ## Needed if running on a self-hosted runner: diff --git a/package/rattler-build/osx/create_bundle.sh b/package/rattler-build/osx/create_bundle.sh index 54d7739d7efd..b45e58ca1736 100644 --- a/package/rattler-build/osx/create_bundle.sh +++ b/package/rattler-build/osx/create_bundle.sh @@ -76,9 +76,9 @@ if [ -d "${conda_env}/PlugIns" ]; then mv ${conda_env}/PlugIns ${conda_env}/.. fi -if [[ "${SIGN_RELEASE}" == "true" ]]; then +if [[ "${MACOS_SIGN_RELEASE}" == "true" ]]; then # create the signed dmg - ../../scripts/macos_sign_and_notarize.zsh -p "FreeCAD" -k ${SIGNING_KEY_ID} -o "${version_name}.dmg" + ../../scripts/macos_sign_and_notarize.zsh -p "FreeCAD" -k ${MACOS_SIGNING_KEY_ID} -o "${version_name}.dmg" else # Ad-hoc sign for local builds (required for QuickLook extensions to register) if [ -d "FreeCAD.app/Contents/PlugIns" ]; then diff --git a/package/rattler-build/windows/create_bundle.sh b/package/rattler-build/windows/create_bundle.sh index ddb69e87bd8d..7b441a3474be 100644 --- a/package/rattler-build/windows/create_bundle.sh +++ b/package/rattler-build/windows/create_bundle.sh @@ -44,6 +44,9 @@ find ${copy_dir} -name \*arm\*.exe -delete # arm binaries that fail to extract u mv ${copy_dir}/bin/Lib/ssl.py .ssl-orig.py cp ssl-patch.py ${copy_dir}/bin/Lib/ssl.py +# Turn off the echo before we start actually calling "echo" +set +x + echo '[Paths]' >> ${copy_dir}/bin/qt6.conf echo 'Prefix = ../lib/qt6' >> ${copy_dir}/bin/qt6.conf @@ -67,6 +70,63 @@ sed -i '1s/.*/\nLIST OF PACKAGES:/' ${copy_dir}/packages.txt mv ${copy_dir} ${version_name} + +# Sign the EXE, DLL, and PYD files (if we can access the Azure account for signing): +set -euo pipefail +SIGN_DIR="${version_name}" + +TENANT="$(az account show --query tenantId -o tsv)" +export AZURE_IDENTITY_DISABLE_WORKLOAD_IDENTITY=true +export AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY=true +unset AZURE_IDENTITY_LOGGING_ENABLED + +if [[ "${WINDOWS_SIGN_RELEASE:-0}" == "1" ]] && \ + az account get-access-token \ + --tenant "$TENANT" \ + --scope "https://codesigning.azure.net/.default" \ + >/dev/null 2>&1; +then + echo "Azure Artifact Signing access confirmed. Beginning signing process..." + + shopt -s nullglob + + FILES=( + "$SIGN_DIR"/*.exe + "$SIGN_DIR"/bin/*.exe + "$SIGN_DIR"/bin/*.dll + "$SIGN_DIR"/bin/*.pyd + ) + + count=0 + total=${#FILES[@]} + echo "Signing $total files" + for f in ${FILES[@]}; do + ((count+=1)) + echo "Signing [$count/$total]: $f" + sign code artifact-signing \ + --artifact-signing-endpoint "${WINDOWS_AZURE_ENDPOINT}" \ + --artifact-signing-certificate-profile "${WINDOWS_AZURE_CERTIFICATE_PROFILE}" \ + --artifact-signing-account "${WINDOWS_AZURE_SIGNING_ACCOUNT}" \ + --timestamp-url https://timestamp.acs.microsoft.com \ + --timestamp-digest sha256 \ + "$f" >/dev/null 2>&1 + + # Output was redirected to /dev/null because Azure authentication is absurdly noisy, with constant misleading + # "failure" messages about Managed Identity authentication failing. We don't use, or want to use, that + # authentication, and the fact that it fails is not a problem as long as the real authentication succeeds. But, + # we better check the return value to be sure things are working... + rc=$? + echo "'sign code artifact-signing ...' exit code: $rc" + done + + # Manually check the important one! + signtool verify -pa "$SIGN_DIR/bin/FreeCAD.exe" + + echo "Signing completed." +else + echo "No Azure Artifact Signing available -- skipping signing." +fi + 7z a -t7z -mx9 -mmt=${NUMBER_OF_PROCESSORS} ${version_name}.7z ${version_name} -bb # create hash sha256sum ${version_name}.7z > ${version_name}.7z-SHA256.txt @@ -88,6 +148,28 @@ if [ "${MAKE_INSTALLER}" == "true" ]; then -X'SetCompressor /FINAL lzma' \ ../../WindowsInstaller/FreeCAD-installer.nsi mv ../../WindowsInstaller/${version_name}-installer.exe . + echo "Created installer ${version_name}-installer.exe" + + # See if we can sign the installer exe as well: + if [[ "${WINDOWS_SIGN_RELEASE:-0}" == "1" ]] && \ + az account get-access-token \ + --tenant "$TENANT" \ + --scope "https://codesigning.azure.net/.default" \ + >/dev/null 2>&1; + then + echo "Signing the installer..." + sign code artifact-signing \ + --artifact-signing-endpoint "${WINDOWS_AZURE_ENDPOINT}" \ + --artifact-signing-certificate-profile "${WINDOWS_AZURE_CERTIFICATE_PROFILE}" \ + --artifact-signing-account "${WINDOWS_AZURE_SIGNING_ACCOUNT}" \ + --timestamp-url http://timestamp.acs.microsoft.com \ + --timestamp-digest sha256 \ + ${version_name}-installer.exe >/dev/null 2>&1 \ + || { echo "Signing the installer failed!"; exit 1; } + else + echo "No code signing available, leaving the installer unsigned" + fi + sha256sum ${version_name}-installer.exe > ${version_name}-installer.exe-SHA256.txt else echo "Error: Failed to get NsProcess plugin. Aborting installer creation..." @@ -96,8 +178,10 @@ if [ "${MAKE_INSTALLER}" == "true" ]; then fi if [ "${UPLOAD_RELEASE}" == "true" ]; then + echo "Uploading the release..." gh release upload --clobber ${BUILD_TAG} "${version_name}.7z" "${version_name}.7z-SHA256.txt" if [ "${MAKE_INSTALLER}" == "true" ]; then gh release upload --clobber ${BUILD_TAG} "${version_name}-installer.exe" "${version_name}-installer.exe-SHA256.txt" fi + echo "Done uploading" fi