diff --git a/.github/workflows/full-build.yml b/.github/workflows/full-build.yml index 15897898861..0e7c09ecaf4 100644 --- a/.github/workflows/full-build.yml +++ b/.github/workflows/full-build.yml @@ -42,9 +42,12 @@ permissions: id-token: write env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_TYPE: Release OPENSTUDIO_BUILD: build - PY_VERSION: "3.12.2" + OPENSTUDIO_SOURCE: OpenStudio + PYTHON_REQUIRED_VERSION: "3.12.2" + SDKROOT: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk AWS_S3_BUCKET: openstudio-ci-builds TEST_DASHBOARD_RELATIVE: Testing/dashboard/test-dashboard.md CCACHE_SLOPPINESS: pch_defines,time_macros,include_file_mtime,include_file_ctime @@ -60,8 +63,8 @@ env: jobs: linux-build: - name: Build ${{ matrix.display_name }} - runs-on: ${{ matrix.runner }} + name: Build ${{ matrix.pretty }} + runs-on: ${{ matrix.os }} container: image: ${{ matrix.container_image }} options: ${{ matrix.container_options }} --volume /mnt:/mnt @@ -70,8 +73,8 @@ jobs: matrix: include: - platform: centos-9-x64 - display_name: CentOS 9 (AlmaLinux) x64 - runner: ubuntu-22.04 + pretty: CentOS 9 (AlmaLinux) x64 + os: ubuntu-22.04 container_image: nrel/openstudio-cmake-tools:almalinux9-main container_options: "--privileged -u root -e LANG=en_US.UTF-8" test_suffix: CentOS-9 @@ -84,8 +87,8 @@ jobs: max_jobs: 2 exclude_regex: "^BCLFixture.RemoteBCLMetaSearchTest$" - platform: ubuntu-2204-x64 - display_name: Ubuntu 22.04 x64 - runner: ubuntu-22.04 + pretty: Ubuntu 22.04 x64 + os: ubuntu-22.04 container_image: nrel/openstudio-cmake-tools:jammy-main container_options: "--privileged -u root -e LANG=en_US.UTF-8" test_suffix: Ubuntu-2204 @@ -98,8 +101,8 @@ jobs: max_jobs: 2 exclude_regex: "^ModelFixture.PythonPluginInstance_NotPYFile$" - platform: ubuntu-2404-x64 - display_name: Ubuntu 24.04 x64 - runner: ubuntu-24.04 + pretty: Ubuntu 24.04 x64 + os: ubuntu-24.04 container_image: nrel/openstudio-cmake-tools:noble-main container_options: "--privileged -u root -e LANG=en_US.UTF-8" test_suffix: Ubuntu-2404 @@ -112,8 +115,8 @@ jobs: max_jobs: 2 exclude_regex: ${{ '""' }} - platform: ubuntu-2204-arm64 - display_name: Ubuntu 22.04 ARM64 - runner: ubuntu-22.04-arm + pretty: Ubuntu 22.04 ARM64 + os: ubuntu-22.04-arm container_image: nrel/openstudio-cmake-tools:jammy-main container_options: "--privileged -u root -e LANG=en_US.UTF-8" test_suffix: Ubuntu-2204-ARM64 @@ -126,8 +129,8 @@ jobs: max_jobs: 2 exclude_regex: "^(GeometryFixture.Plane_RayIntersection|ISOModelFixture.SimModel|SqlFileFixture.AnnualTotalCosts|OpenStudioCLI.*test_measure_manager)$" - platform: ubuntu-2404-arm64 - display_name: Ubuntu 24.04 ARM64 - runner: ubuntu-24.04-arm + pretty: Ubuntu 24.04 ARM64 + os: ubuntu-24.04-arm container_image: nrel/openstudio-cmake-tools:noble-main container_options: "--privileged -u root -e LANG=en_US.UTF-8" test_suffix: Ubuntu-2404-ARM64 @@ -163,7 +166,7 @@ jobs: echo - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 @@ -171,19 +174,17 @@ jobs: uses: actions/cache@v4 with: path: ~/.ccache - key: ccache-${{ runner.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} + key: ccache-${{ matrix.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} restore-keys: | - ccache-${{ runner.os }}-${{ matrix.platform }}- - save-always: true + ccache-${{ matrix.os }}-${{ matrix.platform }}- - name: Restore Conan cache uses: actions/cache@v4 with: path: ~/.conan2 - key: conan-${{ runner.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} + key: conan-${{ matrix.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} restore-keys: | - conan-${{ runner.os }}-${{ matrix.platform }}- - save-always: true + conan-${{ matrix.os }}-${{ matrix.platform }}- - name: Prepare workspace run: | @@ -236,10 +237,9 @@ jobs: ${{ env.OPENSTUDIO_BUILD }}/radiance*.tar.gz ${{ env.OPENSTUDIO_BUILD }}/radiance*.zip ${{ env.OPENSTUDIO_BUILD }}/openstudio*gems*.tar.gz - key: external-deps-${{ runner.os }}-${{ hashFiles('conan.lock') }} + key: external-deps-${{ matrix.os }}-${{ hashFiles('conan.lock') }} restore-keys: | - external-deps-${{ runner.os }}- - save-always: true + external-deps-${{ matrix.os }}- - name: Restore Generated Embedded Files uses: actions/cache@v4 @@ -247,10 +247,9 @@ jobs: path: | ${{ env.OPENSTUDIO_BUILD }}/src/*/embedded_files ${{ env.OPENSTUDIO_BUILD }}/ruby/engine/embedded_files - key: embedded-files-${{ runner.os }}-${{ hashFiles('resources/**', 'ruby/engine/**', 'src/airflow/**', 'src/energyplus/**', 'src/gbxml/**', 'src/isomodel/**', 'src/model/**', 'src/radiance/**', 'src/sdd/**', 'src/utilities/**') }} + key: embedded-files-${{ matrix.os }}-${{ hashFiles('resources/**', 'ruby/engine/**', 'src/airflow/**', 'src/energyplus/**', 'src/gbxml/**', 'src/isomodel/**', 'src/model/**', 'src/radiance/**', 'src/sdd/**', 'src/utilities/**') }} restore-keys: | - embedded-files-${{ runner.os }}- - save-always: true + embedded-files-${{ matrix.os }}- - name: Configure Conan remotes run: | @@ -280,6 +279,11 @@ jobs: ruby_path=$(command -v ruby) echo "SYSTEM_RUBY_PATH=$ruby_path" >> $GITHUB_ENV + - name: Locate Python + run: | + python_path=$(command -v python3) + echo "SYSTEM_PYTHON_PATH=$python_path" >> $GITHUB_ENV + - name: Configure with CMake working-directory: ${{ env.OPENSTUDIO_BUILD }} run: | @@ -300,7 +304,8 @@ jobs: -DBUILD_PYTHON_BINDINGS:BOOL=ON \ -DDISCOVER_TESTS_AFTER_BUILD:BOOL=ON \ -DBUILD_PYTHON_PIP_PACKAGE:BOOL=${{ matrix.pip_package }} \ - -DPYTHON_VERSION:STRING=${{ env.PY_VERSION }} \ + -DPython_EXECUTABLE:FILEPATH="$SYSTEM_PYTHON_PATH" \ + -DPYTHON_VERSION:STRING=${{ env.PYTHON_REQUIRED_VERSION }} \ -DSYSTEM_RUBY_EXECUTABLE="$SYSTEM_RUBY_PATH" \ "$GITHUB_WORKSPACE" @@ -325,6 +330,8 @@ jobs: exit $BUILD_EXIT - name: Run CTest suite + id: ctest + continue-on-error: true working-directory: ${{ env.OPENSTUDIO_BUILD }} run: | set -euo pipefail @@ -337,10 +344,12 @@ jobs: exclude_regex="${{ matrix.exclude_regex }}" echo "Running sequential tests..." + export CTEST_OUTPUT_ON_FAILURE=1 + if [ "$exclude_regex" == '""' ] || [ -z "$exclude_regex" ]; then - ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -j 1 --output-on-failure || overall_exit_code=1 + ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -j 1 -T test || overall_exit_code=1 else - ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -E "$exclude_regex" -j 1 --output-on-failure || overall_exit_code=1 + ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -E "$exclude_regex" -j 1 -T test || overall_exit_code=1 fi echo "Running all other tests in parallel..." @@ -350,13 +359,13 @@ jobs: final_exclude="($exclude_regex|$resource_locked_tests)" fi - ctest -C ${{ env.BUILD_TYPE }} -E "$final_exclude" -j ${{ matrix.max_jobs }} --output-on-failure || overall_exit_code=$? + ctest -C ${{ env.BUILD_TYPE }} -E "$final_exclude" -j ${{ matrix.max_jobs }} -T test || overall_exit_code=$? if [ $overall_exit_code -ne 0 ]; then echo "Rerunning failing tests..." - ctest -C ${{ env.BUILD_TYPE }} --rerun-failed --output-on-failure || overall_exit_code=$? + ctest -C ${{ env.BUILD_TYPE }} --rerun-failed -T test --no-compress-output || overall_exit_code=$? fi - exit $overall_exit_code + echo "exit_code=${overall_exit_code}" >> $GITHUB_OUTPUT - name: Wait for network stability if: always() @@ -373,14 +382,70 @@ jobs: ${{ steps.build_path.outputs.path }}/CTestTestfile.cmake if-no-files-found: warn - - name: Create packages + - name: Copy Testing tree with suffix + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + set -euo pipefail + if [ -d "Testing" ]; then + cp -r Testing "Testing-${{ matrix.test_suffix }}" + fi + + - name: Generate test summary + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + set -euo pipefail + + # Generate a simple markdown summary from CTest results + mkdir -p "$(dirname '${{ env.TEST_DASHBOARD_RELATIVE }}')" + + echo "# OpenStudio Test Results - ${{ matrix.test_suffix }}" > "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Build:** \`${{ github.sha }}\`" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Branch:** \`${{ github.ref_name }}\`" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Platform:** ${{ matrix.pretty }}" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Date:** $(date -u)" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + + if [ -f Testing/Temporary/LastTest.log ]; then + echo "## Test Log (Last 50 lines)" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo '```' >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + tail -50 Testing/Temporary/LastTest.log >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo '```' >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + fi + continue-on-error: true + + - name: Upload Testing artifact if: always() + uses: actions/upload-artifact@v4 + with: + name: Testing-${{ matrix.platform }}-${{ github.sha }} + path: | + ${{ env.OPENSTUDIO_BUILD }}/Testing-${{ matrix.test_suffix }}/ + ${{ env.OPENSTUDIO_BUILD }}/${{ env.TEST_DASHBOARD_RELATIVE }} + + - name: Create packages + if: ${{ success() && !cancelled() }} working-directory: ${{ env.OPENSTUDIO_BUILD }} run: | set -euo pipefail . ./conanbuild.sh cmake --build . --target package + - name: Cleanup intermediate files + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + find . -name "*.o" -type f -delete || true + df -h . + + - name: Fail job on test failures + if: ${{ steps.ctest.outputs.exit_code != '0' }} + run: | + echo "::error::CTest suite failed with exit code ${{ steps.ctest.outputs.exit_code }}" + exit 1 + - name: Upload DEB installer uses: actions/upload-artifact@v4 with: @@ -413,7 +478,7 @@ jobs: name: Publish Linux Artifacts needs: [linux-build] runs-on: ubuntu-latest - if: github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' steps: - name: Download all installers uses: actions/download-artifact@v4 @@ -446,289 +511,305 @@ jobs: done macos-build: - name: ${{ matrix.display_name }} - runs-on: ${{ matrix.runner }} + name: Build Packages for ${{ matrix.pretty }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.allow_failure }} strategy: fail-fast: false matrix: + macos_dev_target: [12.1, 13.0] include: - - platform: macos-x64 - display_name: macOS x64 (Intel) - runner: macos-15-intel + - macos_dev_target: 12.1 + platform: macos-x64 + pretty: "macOS x64" + os: macos-15-intel + allow_failure: false test_suffix: macOS-x64 + arch: x86_64 + python-arch: x64 dmg_glob: "*.dmg" tar_glob: "*OpenStudio*x86_64.tar.gz" exclude_regex: ${{ '""' }} max_jobs: 2 - - platform: macos-arm64 - display_name: macOS ARM64 (Apple Silicon) - runner: macos-15 + - macos_dev_target: 13.0 + platform: macos-arm64 + pretty: "macOS ARM64" + os: macos-15 + allow_failure: false test_suffix: macOS-arm64 + arch: arm64 + python-arch: arm64 dmg_glob: "*.dmg" tar_glob: "*OpenStudio*arm64.tar.gz" exclude_regex: "^(GeometryFixture.Plane_RayIntersection|ISOModelFixture.SimModel)$" max_jobs: 2 - defaults: - run: - shell: bash env: MAX_BUILD_THREADS: ${{ matrix.max_jobs }} CTEST_PARALLEL_LEVEL: ${{ matrix.max_jobs }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: ccache-${{ runner.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} - max-size: ${{ env.CCACHE_MAXSIZE }} - - - name: Restore Conan cache - uses: actions/cache@v4 - with: - path: ~/.conan2 - key: conan-${{ runner.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} - restore-keys: | - conan-${{ runner.os }}-${{ matrix.platform }}- - save-always: true - - - name: Prepare workspace - run: | - set -euo pipefail - git config --global --add safe.directory "$GITHUB_WORKSPACE" - mkdir -p "$GITHUB_WORKSPACE/${{ env.OPENSTUDIO_BUILD }}" - - - name: Cache External Dependencies - uses: actions/cache@v4 - with: - path: | - ${{ env.OPENSTUDIO_BUILD }}/EnergyPlus*.tar.gz - ${{ env.OPENSTUDIO_BUILD }}/EnergyPlus*.zip - ${{ env.OPENSTUDIO_BUILD }}/radiance*.tar.gz - ${{ env.OPENSTUDIO_BUILD }}/radiance*.zip - ${{ env.OPENSTUDIO_BUILD }}/openstudio*gems*.tar.gz - key: external-deps-${{ runner.os }}-${{ hashFiles('conan.lock') }} - restore-keys: | - external-deps-${{ runner.os }}- - save-always: true - - - name: Restore Generated Embedded Files - uses: actions/cache@v4 - with: - path: | - ${{ env.OPENSTUDIO_BUILD }}/src/*/embedded_files - ${{ env.OPENSTUDIO_BUILD }}/ruby/engine/embedded_files - key: embedded-files-${{ runner.os }}-${{ hashFiles('resources/**', 'ruby/engine/**', 'src/airflow/**', 'src/energyplus/**', 'src/gbxml/**', 'src/isomodel/**', 'src/model/**', 'src/radiance/**', 'src/sdd/**', 'src/utilities/**') }} - restore-keys: | - embedded-files-${{ runner.os }}- - save-always: true - - - name: Set up Python 3.12.2 - uses: actions/setup-python@v6 - with: - python-version: '3.12.2' - cache: 'pip' - - - name: Install Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2.2' - bundler-cache: true - - - name: Install Python dependencies - run: | - set -euo pipefail - pip install --upgrade pip setuptools wheel - pip install -r python/requirements.txt - - - name: Install Conan - run: | - set -euo pipefail - pip3 install conan - - - name: Configure Conan remotes - run: | - set -euo pipefail - conan remote add conancenter https://center2.conan.io --force - conan remote add nrel-v2 https://conan.openstudio.net/artifactory/api/conan/conan-v2 --force - if [ ! -f "$HOME/.conan2/profiles/default" ]; then - conan profile detect - fi - - - name: Conan install - run: | - set -euo pipefail - CMAKE_POLICY_VERSION_MINIMUM=3.5 conan install . \ - --output-folder="${{ env.OPENSTUDIO_BUILD }}" \ - --build=missing \ - -c tools.cmake.cmaketoolchain:generator=Ninja \ - -s compiler.cppstd=20 \ - -s build_type=${{ env.BUILD_TYPE }} \ - -o readline/*:with_library=termcap - - - name: Locate Ruby - run: | - ruby_path=$(command -v ruby) - echo "SYSTEM_RUBY_PATH=$ruby_path" >> $GITHUB_ENV - - - name: Determine OpenStudio Version - id: version - run: | - # Extract version from CMakeLists.txt - VERSION_MAJOR_MINOR_PATCH=$(grep "project(OpenStudio VERSION" CMakeLists.txt | sed 's/.*VERSION \([0-9.]*\)).*/\1/') - PRERELEASE=$(grep "set(PROJECT_VERSION_PRERELEASE" CMakeLists.txt | sed 's/.*"\(.*\)").*/\1/') - - if [ -n "$PRERELEASE" ]; then - OPENSTUDIO_VERSION="${VERSION_MAJOR_MINOR_PATCH}-${PRERELEASE}" - else - OPENSTUDIO_VERSION="${VERSION_MAJOR_MINOR_PATCH}" - fi - - echo "Detected OpenStudio Version: $OPENSTUDIO_VERSION" - echo "OPENSTUDIO_VERSION=$OPENSTUDIO_VERSION" >> $GITHUB_ENV - - - name: Configure with CMake - working-directory: ${{ env.OPENSTUDIO_BUILD }} - run: | - set -euo pipefail - . ./conanbuild.sh - # Use absolute path for ccache to avoid resolution issues in containers with symlinked build dirs - CCACHE_ARGS=() - if command -v ccache >/dev/null 2>&1; then - CCACHE_EXE=$(command -v ccache) - CCACHE_ARGS=("-DCMAKE_C_COMPILER_LAUNCHER=$CCACHE_EXE" "-DCMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_EXE") - fi - cmake -G Ninja \ - "${CCACHE_ARGS[@]}" \ - -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake \ - -DCMAKE_BUILD_TYPE:STRING=${{ env.BUILD_TYPE }} \ - -DBUILD_TESTING:BOOL=ON \ - -DCPACK_BINARY_DRAGNDROP:BOOL=ON \ - -DCPACK_BINARY_TGZ:BOOL=ON \ - -DCPACK_BINARY_IFW:BOOL=OFF \ - -DCPACK_PACKAGING_INSTALL_PREFIX="/OpenStudio-${{ env.OPENSTUDIO_VERSION }}" \ - -DBUILD_PYTHON_BINDINGS:BOOL=ON \ - -DDISCOVER_TESTS_AFTER_BUILD:BOOL=ON \ - -DBUILD_PYTHON_PIP_PACKAGE:BOOL=OFF \ - -DPYTHON_VERSION:STRING=${{ env.PY_VERSION }} \ - -DSYSTEM_RUBY_EXECUTABLE="$SYSTEM_RUBY_PATH" \ - "${{ github.workspace }}" - - - name: Build with Ninja - working-directory: ${{ env.OPENSTUDIO_BUILD }} - run: | - set -euo pipefail - . ./conanbuild.sh - export NINJA_STATUS="[%f/%t | %es elapsed | %o objs/sec]" - while true; do - sleep 300 - echo "[heartbeat] $(date -u +"%H:%M:%S")" - if command -v top >/dev/null 2>&1; then top -l 1 -s 0 | grep PhysMem || true; fi - df -h . | tail -1 | awk '{print "[disk] used=" $3 "/" $2 " (" $5 ")"}' - if command -v ps >/dev/null 2>&1; then ps -eo pid,pmem,rss,comm | sort -rn -k2 | head -n 5; fi - done & - heartbeat_pid=$! - cmake --build . --parallel ${MAX_BUILD_THREADS} 2>&1 | tee build.log - build_exit=${PIPESTATUS[0]} - kill $heartbeat_pid || true - command -v ninja >/dev/null 2>&1 && ninja -d stats || true - if [ -f build.log ]; then tail -n 40 build.log; fi - exit $build_exit - - name: Wait for network stability - if: always() - run: sleep 5 - - - name: Upload build diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: build-diag-${{ matrix.platform }}-${{ github.sha }} - path: | - ${{ env.OPENSTUDIO_BUILD }}/build.log - ${{ env.OPENSTUDIO_BUILD }}/.ninja_log - ${{ env.OPENSTUDIO_BUILD }}/CTestTestfile.cmake - if-no-files-found: warn - - - name: Run CTest suite - id: mac_ctest - working-directory: ${{ env.OPENSTUDIO_BUILD }} - continue-on-error: true - run: | - set -euo pipefail - . ./conanbuild.sh - df -h . - - # Conflicting tests that must run sequentially - resource_locked_tests="ModelFixture.ScheduleFile|ModelFixture.ScheduleFileAltCtor|ModelFixture.PythonPluginInstance|ModelFixture.PythonPluginInstance_NotPYFile|ModelFixture.PythonPluginInstance_ClassNameValidation|ModelFixture.ChillerElectricASHRAE205_GettersSetters|ModelFixture.ChillerElectricASHRAE205_Loops|ModelFixture.ChillerElectricASHRAE205_NotCBORFile|ModelFixture.ChillerElectricASHRAE205_Clone" - - overall_exit_code=0 - exclude_regex="${{ matrix.exclude_regex }}" - - echo "Running sequential tests..." - if [ "$exclude_regex" == '""' ] || [ -z "$exclude_regex" ]; then - ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -j 1 || overall_exit_code=1 - else - ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -E "$exclude_regex" -j 1 || overall_exit_code=1 - fi - - echo "Running all other tests in parallel..." - export CTEST_OUTPUT_ON_FAILURE=1 - export CTEST_PARALLEL_LEVEL=${{ matrix.max_jobs }} - - if [ -n "$exclude_regex" ] && [ "$exclude_regex" != '""' ]; then - final_exclude="($exclude_regex|$resource_locked_tests)" - else - final_exclude="^($resource_locked_tests)$" - fi - - ctest -C ${{ env.BUILD_TYPE }} -E "$final_exclude" || overall_exit_code=$? - - if [ $overall_exit_code -ne 0 ]; then - echo "Rerunning failing tests..." - ctest -C ${{ env.BUILD_TYPE }} --rerun-failed --output-on-failure || overall_exit_code=$? - fi - - echo "exit_code=${overall_exit_code}" >> $GITHUB_OUTPUT - - - name: Create packages - if: always() - working-directory: ${{ env.OPENSTUDIO_BUILD }} - run: | - set -euo pipefail - . ./conanbuild.sh - cmake --build . --target package - - - name: Cleanup intermediate files - if: always() - working-directory: ${{ env.OPENSTUDIO_BUILD }} - run: | - find . -name "*.o" -type f -delete || true - df -h . - - - name: Code sign and notarize macOS packages - if: ${{ github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' }} - working-directory: ${{ env.OPENSTUDIO_BUILD }} - env: - APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} - APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} - APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - run: | - set -euo pipefail - - # Check if signing credentials are configured - if [ -z "$APPLE_CERT_DATA" ] || [ -z "$APPLE_CERT_PASSWORD" ]; then - echo "::warning::Apple signing certificates not configured" - echo "::warning::Skipping code signing. Configure APPLE_CERT_DATA and APPLE_CERT_PASSWORD secrets." - exit 0 - fi - + permissions: + # Needed permission to upload the release asset + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + path: ${{ env.OPENSTUDIO_SOURCE }} + fetch-depth: 1 + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ccache-${{ matrix.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} + max-size: ${{ env.CCACHE_MAXSIZE }} + + - name: Restore Conan cache + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: conan-${{ matrix.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} + restore-keys: | + conan-${{ matrix.os }}-${{ matrix.platform }}- + + - name: Cache External Dependencies + uses: actions/cache@v4 + with: + path: | + ${{ env.OPENSTUDIO_BUILD }}/EnergyPlus*.tar.gz + ${{ env.OPENSTUDIO_BUILD }}/EnergyPlus*.zip + ${{ env.OPENSTUDIO_BUILD }}/radiance*.tar.gz + ${{ env.OPENSTUDIO_BUILD }}/radiance*.zip + ${{ env.OPENSTUDIO_BUILD }}/openstudio*gems*.tar.gz + key: external-deps-${{ matrix.os }}-${{ hashFiles('conan.lock') }} + restore-keys: | + external-deps-${{ matrix.os }}- + + - name: Restore Generated Embedded Files + uses: actions/cache@v4 + with: + path: | + ${{ env.OPENSTUDIO_BUILD }}/src/*/embedded_files + ${{ env.OPENSTUDIO_BUILD }}/ruby/engine/embedded_files + key: embedded-files-${{ matrix.os }}-${{ hashFiles('resources/**', 'ruby/engine/**', 'src/airflow/**', 'src/energyplus/**', 'src/gbxml/**', 'src/isomodel/**', 'src/radiance/**', 'src/sdd/**', 'src/model/**', 'src/utilities/**') }} + restore-keys: | + embedded-files-${{ matrix.os }}- + + - name: Remove python ${{ env.PYTHON_REQUIRED_VERSION }} from the toolcache + run: | + ls $RUNNER_TOOL_CACHE/Python || true + rm -Rf "$RUNNER_TOOL_CACHE/Python/${{ env.PYTHON_REQUIRED_VERSION }}" + rm -Rf "$RUNNER_TOOL_CACHE/Python/${{ env.PYTHON_REQUIRED_VERSION }}*/" + + - name: Set up Python ${{ env.PYTHON_REQUIRED_VERSION }} + id: setup-python + uses: jmarrec/setup-python@v5.4.0 + with: + python-version: ${{ env.PYTHON_REQUIRED_VERSION }} + # check-latest: true # Force pick up the python I built instead of the (potential) toolcache one. I could also do `rm -Rf $RUNNER_TOOL_CACHE/Python/3.12.2` before this action + + - name: Install Python dependencies + working-directory: ${{ env.OPENSTUDIO_SOURCE }} + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + pip install -r python/requirements.txt + pip install conan aqtinstall + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2.2' + bundler-cache: true + + - name: Set up Qt IFW + run: | + set -euo pipefail + cd $RUNNER_TEMP + mkdir QtIFW && cd QtIFW + aria2c https://github.com/jmarrec/QtIFW-fixup/releases/download/v5.0.0-dev-with-fixup/QtIFW-5.0.0-${{ matrix.arch }}.zip + xattr -r -d com.apple.quarantine ./QtIFW-5.0.0-${{ matrix.arch }}.zip + unzip QtIFW-5.0.0-${{ matrix.arch }}.zip + rm -Rf ./*.zip + chmod +x * + ./installerbase --version + echo "$(pwd)" >> $GITHUB_PATH + + - name: Install System dependencies + shell: bash + run: | + # The MACOSX_DEPLOYMENT_TARGET environment variable sets the default value for the CMAKE_OSX_DEPLOYMENT_TARGET variable. + # We use cmake commands to build some subprojects, so setting it globally + echo MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_dev_target }} >> $GITHUB_ENV + brew install ninja + + - name: Create Build Directory + run: cmake -E make_directory ${{ env.OPENSTUDIO_BUILD }} + + - name: Configure Conan remotes + run: | + set -euo pipefail + conan remote add --index 0 --force nrel-v2 https://conan.openstudio.net/artifactory/api/conan/conan-v2 + conan remote add --force conancenter https://center2.conan.io + if [ ! -f "$HOME/.conan2/profiles/default" ]; then + conan profile detect + fi + conan config home + + - name: Conan install + working-directory: ${{ env.OPENSTUDIO_SOURCE }} + run: | + set -euo pipefail + CMAKE_POLICY_VERSION_MINIMUM=3.5 conan install . \ + --output-folder=../${{ env.OPENSTUDIO_BUILD }} \ + --build=missing \ + -c tools.cmake.cmaketoolchain:generator=Ninja \ + -s compiler.cppstd=20 \ + -s build_type=${{ env.BUILD_TYPE }} \ + -o readline/*:with_library=termcap + + - name: Locate Ruby + run: | + ruby_path=$(command -v ruby) + echo "SYSTEM_RUBY_PATH=$ruby_path" >> $GITHUB_ENV + + - name: Locate Python + run: | + python_path=$(command -v python3) + echo "SYSTEM_PYTHON_PATH=$python_path" >> $GITHUB_ENV + + - name: Install Documentation Dependencies (Mac) + run: | + brew install --cask basictex + echo "/Library/TeX/texbin" >> $GITHUB_PATH + + - name: Configure with CMake + working-directory: ${{ env.OPENSTUDIO_BUILD }} + env: + APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + run: | + set -e + chmod +x ./conanbuild.sh + . ./conanbuild.sh + # Use absolute path for ccache to avoid resolution issues in containers with symlinked build dirs + CCACHE_ARGS=() + if command -v ccache >/dev/null 2>&1; then + CCACHE_EXE=$(command -v ccache) + CCACHE_ARGS=("-DCMAKE_C_COMPILER_LAUNCHER=$CCACHE_EXE" "-DCMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_EXE") + fi + + # Configure signing identity + SIGNING_ARGS=() + if [ -n "$APPLE_DEV_ID" ]; then + echo "Configuring for signed build" + SIGNING_ARGS=("-DCPACK_IFW_PACKAGE_SIGNING_IDENTITY=$APPLE_DEV_ID") + else + echo "Configuring for ad-hoc signed build (skipping CPack signing identity)" + SIGNING_ARGS=() + fi + + cmake -G Ninja \ + "${CCACHE_ARGS[@]}" \ + "${SIGNING_ARGS[@]}" \ + -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake \ + -DENABLE_COVERAGE:BOOL=OFF \ + -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=${{ matrix.macos_dev_target }} \ + -DCMAKE_BUILD_TYPE:STRING=${{ env.BUILD_TYPE }} \ + -DBUILD_TESTING:BOOL=ON \ + -DCPACK_BINARY_TGZ:BOOL=ON \ + -DCPACK_BINARY_IFW:BOOL=ON \ + -DBUILD_PYTHON_BINDINGS:BOOL=ON \ + -DDISCOVER_TESTS_AFTER_BUILD:BOOL=ON \ + -DBUILD_PYTHON_PIP_PACKAGE:BOOL=OFF \ + -DCMAKE_INSTALL_RPATH="@loader_path;@executable_path" \ + -DPYTHON_VERSION:STRING=${{ env.PYTHON_REQUIRED_VERSION }} \ + -DPython_ROOT_DIR:PATH="$(dirname $(dirname $SYSTEM_PYTHON_PATH))" \ + ../${{ env.OPENSTUDIO_SOURCE }} + + - name: Build with Ninja + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + . ./conanbuild.sh + export NINJA_STATUS="[%f/%t | %es elapsed | %o objs/sec]" + while true; do + sleep 300 + echo "[heartbeat] $(date -u +"%H:%M:%S")" + if command -v top >/dev/null 2>&1; then top -l 1 -s 0 | grep PhysMem || true; fi + df -h . | tail -1 | awk '{print "[disk] used=" $3 "/" $2 " (" $5 ")"}' + if command -v ps >/dev/null 2>&1; then ps -eo pid,pmem,rss,comm | sort -rn -k2 | head -n 5; fi + done & + heartbeat_pid=$! + cmake --build . --parallel ${MAX_BUILD_THREADS} 2>&1 | tee build.log + build_exit=${PIPESTATUS[0]} + kill $heartbeat_pid || true + command -v ninja >/dev/null 2>&1 && ninja -d stats || true + if [ -f build.log ]; then tail -n 40 build.log; fi + exit $build_exit + + - name: Wait for network stability + if: always() + run: sleep 5 + + - name: Upload build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: build-diag-${{ matrix.platform }}-${{ github.sha }} + path: | + ${{ env.OPENSTUDIO_BUILD }}/build.log + ${{ env.OPENSTUDIO_BUILD }}/.ninja_log + ${{ env.OPENSTUDIO_BUILD }}/CTestTestfile.cmake + if-no-files-found: warn + + - name: Run CTest suite + id: mac_ctest + working-directory: ${{ env.OPENSTUDIO_BUILD }} + shell: bash + continue-on-error: true + run: | + set -euo pipefail + gem install bundler -v 2.4.10 --conservative --no-document + + # Conflicting tests that must run sequentially + resource_locked_tests="ModelFixture.ScheduleFile|ModelFixture.ScheduleFileAltCtor|ModelFixture.PythonPluginInstance|ModelFixture.PythonPluginInstance_NotPYFile|ModelFixture.PythonPluginInstance_ClassNameValidation|ModelFixture.ChillerElectricASHRAE205_GettersSetters|ModelFixture.ChillerElectricASHRAE205_Loops|ModelFixture.ChillerElectricASHRAE205_NotCBORFile|ModelFixture.ChillerElectricASHRAE205_Clone" + + overall_exit_code=0 + exclude_regex="${{ matrix.exclude_regex }}" + + echo "Running sequential tests..." + if [ "$exclude_regex" == '""' ] || [ -z "$exclude_regex" ]; then + ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -j 1 -T test || overall_exit_code=1 + else + ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -E "$exclude_regex" -j 1 -T test || overall_exit_code=1 + fi + + echo "Running all other tests in parallel..." + export CTEST_OUTPUT_ON_FAILURE=1 + export CTEST_PARALLEL_LEVEL=${{ matrix.max_jobs }} + + if [ -n "$exclude_regex" ] && [ "$exclude_regex" != '""' ]; then + final_exclude="($exclude_regex|$resource_locked_tests)" + else + final_exclude="^($resource_locked_tests)$" + fi + + ctest -C ${{ env.BUILD_TYPE }} -E "$final_exclude" -T test || overall_exit_code=$? + + if [ $overall_exit_code -ne 0 ]; then + echo "Rerunning failing tests..." + ctest -C ${{ env.BUILD_TYPE }} --rerun-failed -T test --no-compress-output --output-on-failure || overall_exit_code=$? + fi + + echo "exit_code=${overall_exit_code}" >> $GITHUB_OUTPUT + + - name: Setup Keychain + if: ${{ success() && !cancelled() && (github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' || matrix.os == 'macOS') }} + env: + APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} + APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} + run: | + set -euo pipefail + if [ -n "$APPLE_CERT_DATA" ] && [ -n "$APPLE_CERT_PASSWORD" ]; then # Create temporary keychain KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain" KEYCHAIN_PASSWORD=$(openssl rand -base64 32) @@ -741,111 +822,177 @@ jobs: echo "$APPLE_CERT_DATA" | base64 --decode > "$CERT_PATH" security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychain -d user | sed s/\"//g) - # Sign DMG files - mkdir -p signed - for dmg in ${{ matrix.dmg_glob }}; do - if [ -f "$dmg" ]; then - echo "Signing $dmg..." + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV + fi + + - name: Create packages + if: ${{ success() && !cancelled() }} + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + set -euo pipefail + . ./conanbuild.sh + ninja -j ${{ matrix.max_jobs }} package + + - name: Cleanup intermediate files + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + find . -name "*.o" -type f -delete || true + df -h . + + - name: Sign DMG and Notarize + if: ${{ steps.create_packages.outcome == 'success' && (github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' || matrix.os == 'macOS') }} + working-directory: ${{ env.OPENSTUDIO_BUILD }} + env: + APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} + APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + run: | + set -euo pipefail + + # Check if signing credentials are configured + if [ -z "$APPLE_CERT_DATA" ] || [ -z "$APPLE_DEV_ID" ]; then + echo "::warning::Apple signing certificates not configured. Falling back to ad-hoc signing." + export AD_HOC_SIGNING=true + else + export AD_HOC_SIGNING=false + fi + + # Sign DMG files + mkdir -p signed + for dmg in ${{ matrix.dmg_glob }}; do + if [ -f "$dmg" ]; then + echo "Processing $dmg..." + + # The inner app should already be signed by CPack if configured correctly + # We now sign the DMG itself (or ad-hoc sign if needed) + + echo "Signing $dmg..." + if [ "$AD_HOC_SIGNING" = "false" ]; then codesign --force --sign "$APPLE_DEV_ID" --timestamp --options runtime "$dmg" || { echo "::warning::Failed to sign $dmg" cp "$dmg" "signed/$(basename "$dmg")" continue } + else + echo "Applying simple ad-hoc signature to $dmg" + codesign --force --sign - "$dmg" || { + echo "::warning::Failed to ad-hoc sign $dmg" + cp "$dmg" "signed/$(basename "$dmg")" + continue + } + fi + + # Notarize if credentials available (Skip for ad-hoc) + if [ "$AD_HOC_SIGNING" = "false" ] && [ -n "$APPLE_ID_USERNAME" ] && [ -n "$APPLE_ID_PASSWORD" ]; then + echo "Notarizing $dmg..." + xcrun notarytool submit "$dmg" \ + --apple-id "$APPLE_ID_USERNAME" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait || echo "::warning::Notarization failed for $dmg" - # Notarize if credentials available - if [ -n "$APPLE_ID_USERNAME" ] && [ -n "$APPLE_ID_PASSWORD" ]; then - echo "Notarizing $dmg..." - xcrun notarytool submit "$dmg" \ - --apple-id "$APPLE_ID_USERNAME" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait || echo "::warning::Notarization failed for $dmg" - - # Staple the notarization ticket - xcrun stapler staple "$dmg" || echo "::warning::Stapling failed for $dmg" - fi - - cp "$dmg" "signed/$(basename "$dmg")" + # Staple the notarization ticket + xcrun stapler staple "$dmg" || echo "::warning::Stapling failed for $dmg" + elif [ "$AD_HOC_SIGNING" = "true" ]; then + echo "Skipping notarization due to ad-hoc signing." fi - done - - # Cleanup + + cp "$dmg" "signed/$(basename "$dmg")" + fi + done + + # Cleanup + if [ -n "$KEYCHAIN_PATH" ]; then security delete-keychain "$KEYCHAIN_PATH" || true - rm -f "$CERT_PATH" - - echo "Code signing completed" - - - name: Copy Testing tree with suffix - if: always() - working-directory: ${{ env.OPENSTUDIO_BUILD }} - run: | - set -euo pipefail + fi + + echo "Code signing completed" + + - name: Copy Testing tree with suffix + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + set -euo pipefail + if [ -d "Testing" ]; then cp -r Testing "Testing-${{ matrix.test_suffix }}" - - - name: Generate test summary - if: always() - working-directory: ${{ env.OPENSTUDIO_BUILD }} - run: | - set -euo pipefail - - # Generate a simple markdown summary from CTest results - mkdir -p "$(dirname '${{ env.TEST_DASHBOARD_RELATIVE }}')" - - echo "# OpenStudio Test Results - ${{ matrix.test_suffix }}" > "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo "" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo "**Build:** \`${{ github.sha }}\`" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo "**Branch:** \`${{ github.ref_name }}\`" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo "**Platform:** ${{ matrix.display_name }}" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo "**Date:** $(date -u)" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo "" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - - if [ -f Testing/Temporary/LastTest.log ]; then - echo "## Test Log (Last 50 lines)" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo '```' >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - tail -50 Testing/Temporary/LastTest.log >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - echo '```' >> "${{ env.TEST_DASHBOARD_RELATIVE }}" - fi - continue-on-error: true - - - name: Upload Testing artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: Testing-${{ matrix.platform }}-${{ github.sha }} - path: | - ${{ env.OPENSTUDIO_BUILD }}/Testing-${{ matrix.test_suffix }}/ - ${{ env.OPENSTUDIO_BUILD }}/${{ env.TEST_DASHBOARD_RELATIVE }} - - - name: Upload DMG installer - if: always() - uses: actions/upload-artifact@v4 - with: - name: OS-DMG-${{ matrix.platform }}-${{ github.sha }} - path: ${{ env.OPENSTUDIO_BUILD }}/*.dmg - if-no-files-found: ignore - - - name: Upload TGZ package - if: always() - uses: actions/upload-artifact@v4 - with: - name: OS-TGZ-${{ matrix.platform }}-${{ github.sha }} - path: ${{ env.OPENSTUDIO_BUILD }}/*.tar.gz - if-no-files-found: ignore - - - - - name: Fail job on test failures - if: ${{ steps.mac_ctest.outputs.exit_code != '0' }} - run: | - echo "::error::CTest suite failed with exit code ${{ steps.mac_ctest.outputs.exit_code }}" - exit 1 + fi + + - name: Generate test summary + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + set -euo pipefail + + # Generate a simple markdown summary from CTest results + mkdir -p "$(dirname '${{ env.TEST_DASHBOARD_RELATIVE }}')" + + echo "# OpenStudio Test Results - ${{ matrix.test_suffix }}" > "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Build:** \`${{ github.sha }}\`" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Branch:** \`${{ github.ref_name }}\`" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Platform:** ${{ matrix.pretty }}" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "**Date:** $(date -u)" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo "" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + + if [ -f Testing/Temporary/LastTest.log ]; then + echo "## Test Log (Last 50 lines)" >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo '```' >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + tail -50 Testing/Temporary/LastTest.log >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + echo '```' >> "${{ env.TEST_DASHBOARD_RELATIVE }}" + fi + continue-on-error: true + + - name: Upload Testing artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: Testing-${{ matrix.platform }}-${{ github.sha }} + path: | + ${{ env.OPENSTUDIO_BUILD }}/Testing-${{ matrix.test_suffix }}/ + ${{ env.OPENSTUDIO_BUILD }}/${{ env.TEST_DASHBOARD_RELATIVE }} + + - name: Determine installer path + id: installer_path + if: ${{ !cancelled() }} + run: | + if ls ${{ env.OPENSTUDIO_BUILD }}/signed/*.dmg 1> /dev/null 2>&1; then + echo "path=${{ env.OPENSTUDIO_BUILD }}/signed/*.dmg" >> $GITHUB_OUTPUT + elif ls ${{ env.OPENSTUDIO_BUILD }}/*.dmg 1> /dev/null 2>&1; then + echo "path=${{ env.OPENSTUDIO_BUILD }}/*.dmg" >> $GITHUB_OUTPUT + else + echo "path=${{ env.OPENSTUDIO_BUILD }}/*.zip" >> $GITHUB_OUTPUT + fi + + - name: Upload IFW installer + if: always() + uses: actions/upload-artifact@v4 + with: + name: OS-IFW-${{ matrix.platform }}-${{ github.sha }} + path: ${{ steps.installer_path.outputs.path }} + if-no-files-found: ignore + + - name: Upload TGZ package + if: always() + uses: actions/upload-artifact@v4 + with: + name: OS-TGZ-${{ matrix.platform }}-${{ github.sha }} + path: ${{ env.OPENSTUDIO_BUILD }}/*.tar.gz + if-no-files-found: ignore + + - name: Fail job on test failures + if: ${{ steps.mac_ctest.outputs.exit_code != '0' }} + run: | + echo "::error::CTest suite failed with exit code ${{ steps.mac_ctest.outputs.exit_code }}" + exit 1 macos-publish: name: Publish MacOS Artifacts needs: [macos-build] runs-on: ubuntu-latest - if: github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' steps: - name: Download all installers uses: actions/download-artifact@v4 @@ -879,15 +1026,15 @@ jobs: done windows-build: - name: Build ${{ matrix.display_name }} - runs-on: ${{ matrix.runner }} + name: Build ${{ matrix.pretty }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - platform: windows-2022-x64 - display_name: Windows 2022 x64 - runner: windows-2022 + pretty: Windows 2022 x64 + os: windows-2022 test_suffix: Windows-2022 max_jobs: 3 exclude_regex: "^(RubyTest-Date_Test-ymd_constructor)$" @@ -901,7 +1048,7 @@ jobs: PYTHONUTF8: "1" steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 @@ -909,9 +1056,9 @@ jobs: uses: actions/cache@v4 with: path: ${{ github.workspace }}\.sccache - key: sccache-${{ runner.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} + key: sccache-${{ matrix.os }}-${{ matrix.platform }}-${{ hashFiles('conan.lock') }} restore-keys: | - sccache-${{ runner.os }}-${{ matrix.platform }}- + sccache-${{ matrix.os }}-${{ matrix.platform }}- - name: Patch tests for Windows run: | @@ -979,10 +1126,9 @@ jobs: ${{ env.OPENSTUDIO_BUILD }}/radiance*.tar.gz ${{ env.OPENSTUDIO_BUILD }}/radiance*.zip ${{ env.OPENSTUDIO_BUILD }}/openstudio*gems*.tar.gz - key: external-deps-${{ runner.os }}-${{ hashFiles('conan.lock') }} + key: external-deps-${{ matrix.os }}-${{ hashFiles('conan.lock') }} restore-keys: | - external-deps-${{ runner.os }}- - save-always: true + external-deps-${{ matrix.os }}- - name: Restore Generated Embedded Files uses: actions/cache@v4 @@ -990,10 +1136,9 @@ jobs: path: | ${{ env.OPENSTUDIO_BUILD }}/src/*/embedded_files ${{ env.OPENSTUDIO_BUILD }}/ruby/engine/embedded_files - key: embedded-files-${{ runner.os }}-${{ hashFiles('resources/**', 'ruby/engine/**', 'src/airflow/**', 'src/energyplus/**', 'src/gbxml/**', 'src/isomodel/**', 'src/model/**', 'src/radiance/**', 'src/sdd/**', 'src/utilities/**') }} + key: embedded-files-${{ matrix.os }}-${{ hashFiles('resources/**', 'ruby/engine/**', 'src/airflow/**', 'src/energyplus/**', 'src/gbxml/**', 'src/isomodel/**', 'src/model/**', 'src/radiance/**', 'src/sdd/**', 'src/utilities/**') }} restore-keys: | - embedded-files-${{ runner.os }}- - save-always: true + embedded-files-${{ matrix.os }}- - name: Setup sccache uses: Mozilla-Actions/sccache-action@v0.0.5 @@ -1002,9 +1147,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.conan2 - key: conan-${{ runner.os }}-windows-${{ hashFiles('conan.lock') }} + key: conan-${{ matrix.os }}-windows-${{ hashFiles('conan.lock') }} restore-keys: | - conan-${{ runner.os }}-windows- + conan-${{ matrix.os }}-windows- - name: Set up Python 3.12.2 uses: actions/setup-python@v6 @@ -1050,11 +1195,52 @@ jobs: $rubyPath = (Get-Command ruby).Source "SYSTEM_RUBY_PATH=$rubyPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Locate Python + run: | + $pythonPath = (Get-Command python).Source + "SYSTEM_PYTHON_PATH=$pythonPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Install System dependencies and LaTeX + shell: bash + run: | + set -x + echo "Downloading MiKTeX CLI installer" + # We download from a specific mirror already # TODO: Should store this setup package somewhere ourselves + curl -L -O --retry 5 --retry-connrefused https://ctan.math.utah.edu/ctan/tex-archive/systems/win32/miktex/setup/windows-x64/miktexsetup-5.5.0%2B1763023-x64.zip + unzip miktexsetup-5.5.0%2B1763023-x64.zip + + echo "Setting up the local package directory via download" + ./miktexsetup_standalone.exe --verbose \ + --local-package-repository=C:/ProgramData/MiKTeX-Repo \ + --remote-package-repository="https://ctan.math.utah.edu/ctan/tex-archive/systems/win32/miktex/tm/packages/" \ + --package-set=essential \ + download + + echo "Installing from the local package directory previously set up" + ./miktexsetup_standalone.exe --verbose \ + --local-package-repository=C:/ProgramData/MiKTeX-Repo \ + --package-set=essential \ + --shared \ + install + + echo "Adding MiKTeX bin folder to PATH and to GITHUB_PATH" + echo "C:/Program Files/MiKTeX/miktex/bin/x64/" >> $GITHUB_PATH + export PATH="/c/Program Files/MiKTeX/miktex/bin/x64/:$PATH" + + echo "Configuring MiKTeX to install missing packages on the fly" + initexmf --admin --verbose --set-config-value='[MPM]AutoInstall=1' + + echo "Configure default mirror for packages" + mpm --admin --set-repository="https://ctan.math.utah.edu/ctan/tex-archive/systems/win32/miktex/tm/packages/" + # Avoid annoying warning: "xelatex: major issue: So far, you have not checked for updates as a MiKTeX user." + mpm --find-updates + mpm --admin --find-updates + - name: Configure with CMake working-directory: ${{ env.OPENSTUDIO_BUILD }} run: | $sccacheExe = (Get-Command sccache).Source - & $env:ComSpec /c "call conanbuild.bat && cmake -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=`"$sccacheExe`" -DCMAKE_CXX_COMPILER_LAUNCHER=`"$sccacheExe`" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE:STRING=${{ env.BUILD_TYPE }} -DBUILD_TESTING:BOOL=ON -DCPACK_GENERATOR:STRING=`"NSIS;TGZ`" -DBUILD_PYTHON_BINDINGS:BOOL=ON -DDISCOVER_TESTS_AFTER_BUILD:BOOL=ON -DBUILD_PYTHON_PIP_PACKAGE:BOOL=OFF -DPYTHON_VERSION:STRING=${{ env.PY_VERSION }} -DSYSTEM_RUBY_EXECUTABLE=`"%SYSTEM_RUBY_PATH%`" `"${{ github.workspace }}`"" + & $env:ComSpec /c "call conanbuild.bat && cmake -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=`"$sccacheExe`" -DCMAKE_CXX_COMPILER_LAUNCHER=`"$sccacheExe`" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE:STRING=${{ env.BUILD_TYPE }} -DBUILD_TESTING:BOOL=ON -DCPACK_GENERATOR:STRING=`"NSIS;TGZ`" -DBUILD_PYTHON_BINDINGS:BOOL=ON -DDISCOVER_TESTS_AFTER_BUILD:BOOL=ON -DBUILD_PYTHON_PIP_PACKAGE:BOOL=OFF -DPython_EXECUTABLE:FILEPATH=`"$env:SYSTEM_PYTHON_PATH`" -DPYTHON_VERSION:STRING=${{ env.PYTHON_REQUIRED_VERSION }} -DSYSTEM_RUBY_EXECUTABLE=`"%SYSTEM_RUBY_PATH%`" `"${{ github.workspace }}`"" - name: Build with Ninja working-directory: ${{ env.OPENSTUDIO_BUILD }} @@ -1072,6 +1258,8 @@ jobs: if (Get-Command sccache -ErrorAction SilentlyContinue) { sccache -s } - name: Run CTest suite + id: ctest + continue-on-error: true working-directory: ${{ env.OPENSTUDIO_BUILD }} shell: pwsh run: | @@ -1095,12 +1283,14 @@ jobs: $overall_exit_code = 0 $exclude_regex = "${{ matrix.exclude_regex }}" + + $env:CTEST_OUTPUT_ON_FAILURE = "1" Write-Host "Running sequential tests..." if ([string]::IsNullOrEmpty($exclude_regex) -or $exclude_regex -eq '""') { - ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -j 1 --output-on-failure + ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -j 1 -T test } else { - ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -E "$exclude_regex" -j 1 --output-on-failure + ctest -C ${{ env.BUILD_TYPE }} -R "^($resource_locked_tests)$" -E "$exclude_regex" -j 1 -T test } if ($LASTEXITCODE -ne 0) { $overall_exit_code = 1 } @@ -1111,14 +1301,15 @@ jobs: $final_exclude = "($exclude_regex|$resource_locked_tests)" } - ctest -C ${{ env.BUILD_TYPE }} -E "$final_exclude" -j ${{ matrix.max_jobs }} --output-on-failure + ctest -C ${{ env.BUILD_TYPE }} -E "$final_exclude" -j ${{ matrix.max_jobs }} -T test if ($LASTEXITCODE -ne 0) { $overall_exit_code = 1 } if ($overall_exit_code -ne 0) { Write-Host "Rerunning failing tests..." - ctest -C ${{ env.BUILD_TYPE }} --rerun-failed --output-on-failure - if ($LASTEXITCODE -ne 0) { exit 1 } + ctest -C ${{ env.BUILD_TYPE }} --rerun-failed -T test + if ($LASTEXITCODE -ne 0) { $overall_exit_code = 1 } } + "exit_code=$overall_exit_code" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - name: Wait for network stability if: always() @@ -1135,27 +1326,91 @@ jobs: ${{ env.OPENSTUDIO_BUILD }}/CTestTestfile.cmake if-no-files-found: warn + - name: Copy Testing tree with suffix + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + if (Test-Path "Testing") { + Copy-Item -Path "Testing" -Destination "Testing-${{ matrix.test_suffix }}" -Recurse -Force + } + + - name: Generate test summary + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + $dashboardPath = "${{ env.TEST_DASHBOARD_RELATIVE }}" + $dashboardDir = Split-Path -Parent $dashboardPath + if (-not (Test-Path $dashboardDir)) { New-Item -ItemType Directory -Path $dashboardDir -Force } + + "# OpenStudio Test Results - ${{ matrix.test_suffix }}" | Out-File -FilePath $dashboardPath -Encoding utf8 + "" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "**Build:** `${{ github.sha }}`" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "**Branch:** `${{ github.ref_name }}`" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "**Platform:** ${{ matrix.pretty }}" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "**Date:** $(Get-Date -Format u)" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + + if (Test-Path "Testing/Temporary/LastTest.log") { + "## Test Log (Last 50 lines)" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "```" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + Get-Content "Testing/Temporary/LastTest.log" -Tail 50 | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + "```" | Out-File -FilePath $dashboardPath -Encoding utf8 -Append + } + continue-on-error: true + + - name: Upload Testing artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: Testing-${{ matrix.platform }}-${{ github.sha }} + path: | + ${{ env.OPENSTUDIO_BUILD }}/Testing-${{ matrix.test_suffix }}/ + ${{ env.OPENSTUDIO_BUILD }}/${{ env.TEST_DASHBOARD_RELATIVE }} + + - name: Create packages + if: ${{ success() && !cancelled() }} + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + & $env:ComSpec /c "call conanbuild.bat && cmake --build . --target package" + if ($LASTEXITCODE -ne 0) { + Write-Error "CPack failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + + - name: Cleanup intermediate files + if: always() + working-directory: ${{ env.OPENSTUDIO_BUILD }} + run: | + Get-ChildItem -Path . -Include "*.obj" -Recurse -Force | Remove-Item -Force -ErrorAction SilentlyContinue + Get-PSDrive C | Select-Object Used,Free + + - name: Fail job on test failures + if: ${{ steps.ctest.outputs.exit_code != '0' }} + run: | + echo "::error::CTest suite failed with exit code ${{ steps.ctest.outputs.exit_code }}" + exit 1 + # CODE SIGNING - AWS Signing Service - name: Setup Node.js - if: github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' uses: actions/setup-node@v4 with: node-version: "18" - name: Install Signing Client Dependencies - if: github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' run: npm install working-directory: ./.github/signing-client - name: Create .env file for Signing - if: github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' run: | echo "ACCESS_KEY=${{ secrets.AWS_SIGNING_ACCESS_KEY }}" > .env echo "SECRET_KEY=${{ secrets.AWS_SIGNING_SECRET_KEY }}" >> .env working-directory: ${{ env.OPENSTUDIO_BUILD }} - name: Code sign installer - if: success() && (github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true') + if: success() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true') working-directory: ${{ env.OPENSTUDIO_BUILD }} run: | # Check if signing client exists @@ -1214,15 +1469,7 @@ jobs: } } - Write-Host "------------------------------------------------------------" - Write-Host "Step 2: Creating Installer (CPack)" - Write-Host "------------------------------------------------------------" - # FIXED: Use $env:ComSpec to avoid MSYS2 cmd path conflict - & $env:ComSpec /c "call conanbuild.bat && cmake --build . --target package" - if ($LASTEXITCODE -ne 0) { - Write-Error "CPack failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE - } + if ($canSign) { Write-Host "------------------------------------------------------------" @@ -1284,7 +1531,7 @@ jobs: name: Publish Windows Artifacts needs: [windows-build] runs-on: ubuntu-latest - if: github.ref == 'refs/heads/develop' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || inputs.publish_to_s3 == 'true' || github.event.inputs.publish_to_s3 == 'true' steps: - name: Download all installers uses: actions/download-artifact@v4 diff --git a/CODE_SIGNING_SETUP.md b/CODE_SIGNING_SETUP.md deleted file mode 100644 index 7e5dc48334d..00000000000 --- a/CODE_SIGNING_SETUP.md +++ /dev/null @@ -1,279 +0,0 @@ -# Code Signing Setup for OpenStudio Windows Builds - -## Overview - -The OpenStudio workflow uses **AWS Code Signing Service (Amazon Signer)** via a custom Node.js client. This document explains how to set up code signing for GitHub Actions. - -## Current Setup (Identified) - -✅ **Service:** AWS Code Signing (Amazon Signer) -✅ **Client Location:** `C:/code-signing-client/code-signing.js` (on Jenkins) -✅ **Authentication:** Uses `ACCESS_KEY` and `SECRET_KEY` from `.env` file -✅ **Process:** -1. Compresses `.exe` files into a zip archive -2. Uses Node.js script to send archive to AWS signing service -3. Receives back a signed zip file -4. Extracts signed executables - -## Migration to GitHub Actions - -The signing client needs to be moved from the Jenkins server to your GitHub repository, and credentials need to be stored as GitHub secrets. - -## GitHub Actions Setup (AWS Signer) - -### Required Steps - -**1. Copy Signing Client from Jenkins to Repository** - -On your Jenkins Windows runner: -```powershell -cd C:\code-signing-client - -# Remove sensitive files and dependencies -Remove-Item node_modules -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item .env -Force -ErrorAction SilentlyContinue - -# List files to verify -Get-ChildItem - -# Create archive with essential files -Compress-Archive -Path *.js,package.json,package-lock.json -DestinationPath code-signing-client.zip -``` - -In your OpenStudio repository: -```bash -# Create directory for signing client -mkdir -p .github/signing-client - -# Extract the zip contents here -# Should include: code-signing.js, package.json, package-lock.json - -# Add to git -git add .github/signing-client/ -git commit -m "Add AWS code signing client" -``` - -**2. Configure GitHub Secrets** - -Go to: Repository Settings → Secrets and variables → Actions → New repository secret - -Add these secrets from your Jenkins `.env` file: -- `AWS_SIGNING_ACCESS_KEY` - Value from `ACCESS_KEY` in `.env` -- `AWS_SIGNING_SECRET_KEY` - Value from `SECRET_KEY` in `.env` - -**3. Workflow Implementation (Already Included)** - -The `full-build-github-hosted.yml` workflow includes these steps: - -```yaml -- name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - -- name: Install Signing Client Dependencies - run: npm install - working-directory: ./.github/signing-client - -- name: Create .env file for Signing - run: | - echo "ACCESS_KEY=${{ secrets.AWS_SIGNING_ACCESS_KEY }}" > .env - echo "SECRET_KEY=${{ secrets.AWS_SIGNING_SECRET_KEY }}" >> .env - shell: pwsh - working-directory: ./.github/signing-client - -- name: Code sign installer - shell: pwsh - working-directory: ${{ github.workspace }}/${{ env.OPENSTUDIO_BUILD }} - run: | - # Sign build executables - Compress-Archive -Path *.exe -DestinationPath build-${{ github.run_id }}.zip -Force - node "${{ github.workspace }}/.github/signing-client/code-signing.js" "${{ github.workspace }}/${{ env.OPENSTUDIO_BUILD }}/build-${{ github.run_id }}.zip" -t 4800000 - Expand-Archive -Path build-${{ github.run_id }}.signed.zip -Force - - # Re-package with signed binaries - cpack -B . - - # Sign installer - Compress-Archive -Path OpenStudio*.exe -DestinationPath OpenStudio-Installer-${{ github.run_id }}.zip -Force - node "${{ github.workspace }}/.github/signing-client/code-signing.js" "${{ github.workspace }}/${{ env.OPENSTUDIO_BUILD }}/OpenStudio-Installer-${{ github.run_id }}.zip" -t 4800000 - - # Extract signed installer - if (-not (Test-Path signed)) { New-Item -ItemType Directory -Path signed | Out-Null } - Expand-Archive -Path OpenStudio-Installer-${{ github.run_id }}.signed.zip -DestinationPath signed -Force -``` - -### Alternative: Use SignPath.io (If Switching Services) - -SignPath offers free code signing for open source projects. - -**Steps:** -1. Sign up at https://signpath.io -2. Submit your project for OSS approval -3. Get your API token -4. Use their GitHub Action - -**Workflow Implementation:** -```yaml -- name: Sign Windows executable - uses: signpath/github-action-submit-signing-request@v0.3 - with: - api-token: ${{ secrets.SIGNPATH_API_TOKEN }} - organization-id: 'your-org-id' - project-slug: 'OpenStudio' - signing-policy-slug: 'release-signing' - artifact-configuration-slug: 'windows-installer' - input-artifact-path: 'OpenStudio-*.exe' - output-artifact-path: 'signed/OpenStudio-*.exe' -``` - -### Option 3: Use Azure Code Signing (Trusted Signing) - -**Requirements:** -- Azure subscription -- Azure Key Vault -- Code signing certificate stored in Key Vault - -**Workflow Implementation:** -```yaml -- name: Azure Login - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - -- name: Sign with Azure Code Signing - uses: azure/trusted-signing-action@v0.3.16 - with: - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - endpoint: https://xxx.codesigning.azure.net/ - code-signing-account-name: YourAccountName - certificate-profile-name: YourProfileName - files-folder: ${{ github.workspace }}/build - files-folder-filter: exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.digicert.com - timestamp-digest: SHA256 -``` - -### Option 4: Use Windows SDK signtool with Certificate - -If you have a traditional code signing certificate (not cloud-based): - -**Workflow Implementation:** -```yaml -- name: Decode certificate - shell: pwsh - run: | - $bytes = [Convert]::FromBase64String("${{ secrets.CERTIFICATE_BASE64 }}") - [IO.File]::WriteAllBytes("${{ github.workspace }}/cert.pfx", $bytes) - -- name: Sign executable - shell: pwsh - run: | - $signtool = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" - & $signtool sign /f "${{ github.workspace }}/cert.pfx" ` - /p "${{ secrets.CERTIFICATE_PASSWORD }}" ` - /tr http://timestamp.digicert.com ` - /td sha256 ` - /fd sha256 ` - "OpenStudio-*.exe" - -- name: Cleanup - if: always() - shell: pwsh - run: | - Remove-Item "${{ github.workspace }}/cert.pfx" -ErrorAction SilentlyContinue -``` - -## Required GitHub Secrets - -Configure these secrets in your repository (Settings → Secrets and variables → Actions): - -### For AWS Code Signing (Current Setup): -- `AWS_SIGNING_ACCESS_KEY` - AWS Access Key ID for signing service (from Jenkins `.env` ACCESS_KEY) -- `AWS_SIGNING_SECRET_KEY` - AWS Secret Access Key for signing service (from Jenkins `.env` SECRET_KEY) - -### Also Required (for S3 publishing): -- `AWS_ACCESS_KEY_ID` - For S3 artifact uploads -- `AWS_SECRET_ACCESS_KEY` - For S3 artifact uploads -- `AWS_REGION` - Optional, defaults to 'us-west-2' - -### Alternative Services (if switching): -- **SignPath:** `SIGNPATH_API_TOKEN` -- **Azure:** `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` -- **Traditional Certificate:** `CERTIFICATE_BASE64`, `CERTIFICATE_PASSWORD` - -## Next Steps - -1. ✅ **Service Identified:** AWS Code Signing (Amazon Signer) -2. **Copy signing client from Jenkins** to `.github/signing-client/` in repository -3. **Extract credentials from Jenkins `.env` file** (ACCESS_KEY and SECRET_KEY) -4. **Add GitHub secrets:** `AWS_SIGNING_ACCESS_KEY` and `AWS_SIGNING_SECRET_KEY` -5. **Test the signing process** in a test branch before deploying to production -6. **Verify signed executables** using `Get-AuthenticodeSignature` - -## Testing Code Signing - -After setting up the workflow, verify signatures with: - -```powershell -# Download the signed installer from GitHub artifacts -# Then check if file is signed: -Get-AuthenticodeSignature "OpenStudio-*.exe" | Format-List - -# Verify signature details -signtool verify /pa /v "OpenStudio-*.exe" - -# Expected output should show: -# - Status: Valid -# - Signer: Your organization/certificate name -# - Timestamp: Valid timestamp -``` - -## Troubleshooting - -### Common Issues: - -1. **"code-signing-client not found" or "code-signing.js not found"** - - Ensure `.github/signing-client/` directory exists in repository - - Verify files were committed: `git ls-files .github/signing-client/` - - Check file paths in workflow match actual location - -2. **"ACCESS_KEY not found" or authentication failed** - - Verify `AWS_SIGNING_ACCESS_KEY` secret is correctly set in GitHub - - Verify `AWS_SIGNING_SECRET_KEY` secret is correctly set in GitHub - - Check `.env` file is created properly in the workflow step - - Verify credentials have not expired or been rotated - -3. **"Timeout waiting for signed file"** - - Increase timeout value (currently 4800000ms = 80 minutes) - - Check AWS signing service status - - Verify network connectivity from GitHub runners to AWS - -4. **"npm install failed" in signing client** - - Ensure `package.json` and `package-lock.json` are in repository - - Check for Node.js version compatibility - - Review npm install logs for specific dependency errors - -5. **Signature verification fails** - - Check that correct certificate is configured in AWS Signer - - Verify certificate has not expired - - Ensure proper signing profile is being used - -## Additional Resources - -- [GitHub Actions: Encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) -- [Windows Code Signing Best Practices](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/authenticode) -- [AWS Signer Documentation](https://docs.aws.amazon.com/signer/) -- [SignPath Documentation](https://signpath.io/documentation) - Alternative service -- [Azure Trusted Signing](https://learn.microsoft.com/en-us/azure/trusted-signing/) - Alternative service - -## Summary - -✅ **Service:** AWS Code Signing (Amazon Signer) -✅ **Migration:** Copy signing client to `.github/signing-client/` -✅ **Secrets:** `AWS_SIGNING_ACCESS_KEY` and `AWS_SIGNING_SECRET_KEY` -✅ **Workflow:** Already configured in `full-build-github-hosted.yml` -✅ **Testing:** Verify signatures with `Get-AuthenticodeSignature` \ No newline at end of file diff --git a/README.md b/README.md index 8563b03a5eb..043005aef6b 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,27 @@ OpenStudio is a cross-platform (Windows, Mac, and Linux) collection of software The OpenStudio SDK allows building researchers and software developers to quickly get started through its multiple entry levels, including access through C++, Ruby, Python, and C#. More information and documentation is available at the [OpenStudio website](https://www.openstudio.net/). User support is available via the community moderated question and answer resource [unmethours.com](https://unmethours.com/questions/). + +## Installation Notes (macOS) + +For development builds (artifacts downloaded from GitHub Actions), you may encounter a "Damaged" error or "Unidentified Developer" warning on macOS, especially on Apple Silicon (ARM) machines. This is because these builds are not notarized by Apple. + +If you encounter these issues, standard `xattr` commands on the DMG may not be sufficient. Please follow these steps: + +1. **Mount the DMG** image. +2. **Copy** the Installer application (e.g., `OpenStudio-3.11.0...app`) from the mounted volume to a local folder (e.g., your `Downloads` folder). *Do not run it directly from the DMG.* +3. Open a **Terminal** and run the following commands on the *local copy* of the installer: + + ```bash + # 1. Remove quarantine attributes + xattr -cr path/to/local/OpenStudio-Installer.app + + # 2. Ad-hoc sign the application (fixes "Killed" or crashes on startup) + codesign --force --deep --sign - path/to/local/OpenStudio-Installer.app + ``` + +4. Run the installer. If double-clicking fails, run the executable directly with `sudo`: + + ```bash + sudo path/to/local/OpenStudio-Installer.app/Contents/MacOS/OpenStudio-3.11.0--Darwin- + ```