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