Manual CI run #840
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Useful documentation for GitHub Actions workflows: | |
| # https://docs.github.com/en/actions/using-workflows/about-workflows | |
| name: ci | |
| # Useful documentation for `on`: | |
| # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on | |
| on: | |
| # Run CI when a push is made to specific branches only | |
| push: | |
| branches: | |
| - main | |
| - 'copilot/*' | |
| # Allow CI to be run manually in GitHub Actions UI | |
| workflow_dispatch: | |
| inputs: | |
| build_dist_artifact: | |
| description: 'Build distribution artifact for macOS and Windows' | |
| required: false | |
| default: false | |
| type: boolean | |
| build_arch_macos: | |
| description: 'Build architecture for macOS' | |
| required: false | |
| # NOTE: It is faster to build a single architecture only | |
| default: 'arm64' | |
| type: choice | |
| options: | |
| - 'arm64' | |
| # NOTE: Required for building the distribution artifact | |
| - 'universal2' | |
| run_asan_tests: | |
| description: 'Run Address Sanitizer tests' | |
| required: false | |
| default: false | |
| type: boolean | |
| show_notarization_log: | |
| description: 'Show notarization log for macOS' | |
| required: false | |
| default: false | |
| type: boolean | |
| # Customize the run name if the workflow is run manually. | |
| # Otherwise use the default run name (as specified by ''). | |
| run-name: > | |
| ${{ github.event_name == 'workflow_dispatch' && 'Manual CI run' || '' }} | |
| # Set the subset of tests to run across all environments. | |
| # To run all tests, set to an empty string. | |
| env: | |
| TEST_NAMES: "" | |
| # 1. Test Crystal binary on each supported OS | |
| # 2. Test Crystal with Address Sanitizer on macOS specially | |
| # because segfaults more common on macOS | |
| jobs: | |
| # CAUTION: macOS runners cost 10x a Linux runner and 5x a Windows runner. | |
| # Keep the matrix small to avoid high costs. | |
| build-macos-asan: | |
| # Only run this expensive job when explicitly requested or when building distribution artifacts | |
| if: ${{ github.event.inputs.run_asan_tests == 'true' || github.event.inputs.build_dist_artifact == 'true' }} | |
| strategy: | |
| matrix: | |
| python-version: | |
| # TODO: Upgrade to Python 3.14 | |
| # NOTE: py2app 0.28.8 only supports Python 3.6 - 3.13. | |
| - "3.13.5" | |
| fail-fast: false | |
| # NOTE: macos-15 is the last macOS runner planned by GitHub to support Intel rather than arm64 | |
| runs-on: macos-15-intel | |
| timeout-minutes: 60 # high limit; last resort to detect hangs | |
| env: | |
| # Suppress warning "malloc: nano zone abandoned due to inability to preallocate reserved vm space" | |
| # from macOS being confused by Address Sanitizer's changes to malloc. | |
| # https://stackoverflow.com/questions/64126942/malloc-nano-zone-abandoned-due-to-inability-to-preallocate-reserved-vm-space | |
| MallocNanoZone: 0 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Restore cached Python and virtual environment | |
| id: restore-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| cpython | |
| venv_asan | |
| key: build-macos-asan-python-${{ matrix.python-version }} | |
| - name: Compile Python ${{ matrix.python-version }} with Address Sanitizer | |
| if: steps.restore-cache.outputs.cache-hit != 'true' | |
| env: | |
| PYTHON_VERSION: ${{ matrix.python-version }} | |
| run: | | |
| wget --no-verbose https://github.com/python/cpython/archive/refs/tags/v$PYTHON_VERSION.zip | |
| unzip -q v3.*.zip | |
| mv cpython-3.* cpython | |
| cd cpython | |
| ./configure --with-pydebug --with-address-sanitizer | |
| make -s -j3 | |
| ./python.exe -c 'print("OK")' | |
| - name: Create virtual environment | |
| if: steps.restore-cache.outputs.cache-hit != 'true' | |
| run: | | |
| cpython/python.exe -m venv venv_asan | |
| - name: Install non-ASAN Python for Playwright | |
| id: install-nonasan-python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| update-environment: false | |
| - name: Install Poetry | |
| run: | | |
| python -m pip install -U pip setuptools | |
| python -m pip install -U "poetry==2.1.1" | |
| - name: Activate virtual environment | |
| run: | | |
| source venv_asan/bin/activate | |
| echo PATH=$PATH >> $GITHUB_ENV | |
| echo VIRTUAL_ENV=$VIRTUAL_ENV >> $GITHUB_ENV | |
| - name: Install dependencies with Poetry | |
| # If build takes a very long time, then it's likely that the version | |
| # of wxPython installed does not offer a precompiled wheel for this | |
| # version of Python. Check the wxPython PyPI page to confirm. | |
| timeout-minutes: 2 # normally takes 6s, as of 2023-03-03 | |
| run: poetry install | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/Library/Caches/ms-playwright | |
| key: playwright-browsers-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} | |
| - name: Install Playwright browsers | |
| run: poetry run playwright install chromium | |
| - name: Run non-UI tests | |
| # Disable this tests because they are low value in an ASAN context, | |
| # and add several minutes of runtime | |
| if: false | |
| run: poetry run python -m pytest | |
| - name: Run UI tests | |
| id: run_ui_tests | |
| env: | |
| CRYSTAL_FAULTHANDLER: 'True' | |
| CRYSTAL_ADDRESS_SANITIZER: 'True' | |
| # Multiply timeouts because ASan builds of Python are slower than regular builds | |
| CRYSTAL_GLOBAL_TIMEOUT_MULTIPLIER: '2.0' | |
| CRYSTAL_EXCLUDE_TESTS_MARKED: 'slow' | |
| CRYSTAL_NONASAN_PYTHON_PATH: ${{ steps.install-nonasan-python.outputs.python-path }} | |
| run: | | |
| crystal test --parallel ${TEST_NAMES} | |
| build-linux-asan: | |
| # Only run this expensive job when explicitly requested or when building distribution artifacts | |
| if: ${{ github.event.inputs.run_asan_tests == 'true' || github.event.inputs.build_dist_artifact == 'true' }} | |
| strategy: | |
| matrix: | |
| os: | |
| # Same as build-linux job | |
| - ubuntu-22.04 | |
| python-version: | |
| - "3.11.9" | |
| - "3.12.9" | |
| - "3.13.5" | |
| - "3.14.0" | |
| fail-fast: false | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 78 # 150% of normal time: 32-52 min, as of 2025-10-14 | |
| env: | |
| ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Restore cached Python and virtual environment | |
| id: restore-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| cpython | |
| venv_asan | |
| key: build-linux-asan-os-${{ matrix.os }}-python-${{ matrix.python-version }} | |
| - name: Update APT packages | |
| run: sudo apt-get update | |
| # NOTE: Compiler choice taken from: https://github.com/python/cpython/actions/runs/17596932916/workflow | |
| - name: Set up GCC-10 for ASAN | |
| if: steps.restore-cache.outputs.cache-hit != 'true' | |
| uses: egor-tensin/setup-gcc@v1 | |
| with: | |
| version: 10 | |
| - name: Install OpenSSL | |
| run: sudo apt-get install -y openssl | |
| - name: Compile Python ${{ matrix.python-version }} with Address Sanitizer | |
| if: steps.restore-cache.outputs.cache-hit != 'true' | |
| env: | |
| PYTHON_VERSION: ${{ matrix.python-version }} | |
| run: | | |
| wget --no-verbose https://github.com/python/cpython/archive/refs/tags/v$PYTHON_VERSION.zip | |
| unzip -q v3.*.zip | |
| mv cpython-3.* cpython | |
| cd cpython | |
| ./configure --with-address-sanitizer --without-pymalloc | |
| make -s -j3 | |
| ./python -c 'print("OK")' | |
| - name: Create virtual environment | |
| if: steps.restore-cache.outputs.cache-hit != 'true' | |
| run: | | |
| cpython/python -m venv venv_asan | |
| - name: Install non-ASAN Python for Playwright | |
| id: install-nonasan-python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| update-environment: false | |
| - name: Install Poetry | |
| run: | | |
| python -m pip install -U pip setuptools | |
| python -m pip install -U "poetry==2.1.1" | |
| - name: Activate virtual environment | |
| run: | | |
| source venv_asan/bin/activate | |
| echo PATH=$PATH >> $GITHUB_ENV | |
| echo VIRTUAL_ENV=$VIRTUAL_ENV >> $GITHUB_ENV | |
| # HACK: Suppress warning "Error retrieving accessibility bus | |
| # address: org.freedesktop.DBus.Error.ServiceUnknown: | |
| # The name org.a11y.Bus was not provided by any .service files" | |
| # when running tests later | |
| - name: Install at-spi2-core | |
| run: sudo apt-get install -y at-spi2-core | |
| # NOTE: Needed for screenshot support while running tests | |
| # TODO: Is scrot actually still needed? gnome-screenshot definitely is. | |
| - name: Install scrot and gnome-screenshot | |
| run: sudo apt-get install scrot gnome-screenshot | |
| - name: Install wxPython dependencies | |
| run: sudo apt-get install -y libgtk-3-dev | |
| # Install additional dependencies for Python 3.13+ wxPython | |
| - name: Install wxPython dependencies for Python 3.13+ | |
| if: ${{ startsWith(matrix.python-version, '3.13.') || startsWith(matrix.python-version, '3.14.') }} | |
| run: sudo apt-get install -y libnotify4 | |
| - name: Install wagon | |
| run: poetry run pip3 install wagon | |
| - name: Patch wagon 1.0.3 for Python 3.14+ | |
| if: startsWith(matrix.python-version, '3.14.') | |
| run: | | |
| WAGON_PATH=$(poetry run python -c 'import importlib.util; spec = importlib.util.find_spec("wagon"); print(spec.origin)') | |
| sed -i 's/from urllib.request import URLopener/from urllib.request import urlopen/' $WAGON_PATH | |
| sed -i 's/from urllib import urlopen/from urllib.request import urlopen/' $WAGON_PATH | |
| # Install wxPython from precompiled wagon because installing | |
| # wxPython from source takes about 40 minutes on GitHub Actions | |
| # | |
| # NOTE: To recompile the .wgn, see instructions in: doc/how_to_make_wxpython_wagon.md | |
| - name: Install dependency wxPython from wagon (Python 3.11) | |
| if: startsWith(matrix.python-version, '3.11.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py311-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Install dependency wxPython from wagon (Python 3.12) | |
| if: startsWith(matrix.python-version, '3.12.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py312-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Install dependency wxPython from wagon (Python 3.13) | |
| if: startsWith(matrix.python-version, '3.13.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py313-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Install dependency wxPython from wagon (Python 3.14) | |
| if: startsWith(matrix.python-version, '3.14.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py314-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Fail if wxPython wagon is not available for this Python version | |
| if: ${{ !startsWith(matrix.python-version, '3.11.') && !startsWith(matrix.python-version, '3.12.') && !startsWith(matrix.python-version, '3.13.') && !startsWith(matrix.python-version, '3.14.') }} | |
| run: | | |
| echo "ERROR: No precompiled wxPython wagon is available for Python ${{ matrix.python-version }} on Linux." | |
| echo "You must build a .wgn for this Python version and update the workflow." | |
| exit 1 | |
| - name: Install remaining dependencies with Poetry | |
| # If build takes a very long time, the precompiled .wgn in the | |
| # previous build step may no longer be installing the correct | |
| # dependency versions that are consistent with pyproject.toml and | |
| # poetry.lock. In that case you'll need to rebuild the .wgn using | |
| # instructions from: doc/how_to_make_wxpython_wagon.md | |
| timeout-minutes: 2 # normally takes 6s, as of 2023-03-03 | |
| run: poetry install | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-browsers-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} | |
| - name: Install Playwright browsers | |
| run: poetry run playwright install chromium | |
| - name: Run non-UI tests | |
| # Disable this tests because they are low value in an ASAN context, | |
| # and add several minutes of runtime | |
| if: false | |
| run: poetry run python -m pytest | |
| - name: Run UI tests | |
| id: run_ui_tests | |
| env: | |
| CRYSTAL_FAULTHANDLER: 'True' | |
| CRYSTAL_ADDRESS_SANITIZER: 'True' | |
| # Multiply timeouts because ASan builds of Python are slower than regular builds | |
| CRYSTAL_GLOBAL_TIMEOUT_MULTIPLIER: '2.0' | |
| CRYSTAL_EXCLUDE_TESTS_MARKED: 'slow' | |
| CRYSTAL_NONASAN_PYTHON_PATH: ${{ steps.install-nonasan-python.outputs.python-path }} | |
| # Enable a version of malloc() that looks for heap corruption specially: | |
| # https://stackoverflow.com/a/3718867/604063 | |
| MALLOC_CHECK_: '2' | |
| run: | | |
| poetry run -- python3 .github/workflows/watchdog.py --timeout=120 -- \ | |
| poetry run xvfb-run \ | |
| crystal test --parallel ${TEST_NAMES} | |
| # CAUTION: macOS runners cost 10x a Linux runner and 5x a Windows runner. | |
| # Keep the matrix small to avoid high costs. | |
| build-macos: | |
| strategy: | |
| matrix: | |
| os: | |
| # NOTE: Earliest macOS supported by Crystal is macOS 13 (from the README) | |
| #- macos-12 # no longer supported by GitHub Actions after 12/3/2024 | |
| #- macos-13 # last macOS runner to run on Intel rather than arm64; no longer supported by GitHub Actions after 12/4/2025 | |
| - macos-14 # ARM-based | |
| #- macos-15 # ARM-based | |
| # Test the latest supported Python version only | |
| python-version: | |
| # TODO: Upgrade to Python 3.14 | |
| # NOTE: py2app 0.28.8 only supports Python 3.6 - 3.13. | |
| - "3.13.5" | |
| fail-fast: false | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 33 # 150% of normal time: 22 min, as of 2025-09-17 | |
| env: | |
| # Enables capturing and uploading core dumps on macOS | |
| # TODO: Convert this to a workflow_dispatch input so that it can be | |
| # enabled in the GitHub Actions UI. | |
| ENABLE_CORE_DUMPS: 'false' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # NOTE: Step must be after "Set up Python" so that Poetry installs | |
| # itself into that Python | |
| - name: Install Poetry | |
| run: | | |
| python -m pip install -U pip setuptools | |
| python -m pip install -U "poetry==2.1.1" | |
| - name: Download dependencies as universal2 wheels | |
| if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build_dist_artifact == 'true' || github.event.inputs.build_arch_macos == 'universal2') }} | |
| run: | | |
| poetry self add poetry-plugin-export==1.9.0 | |
| python -m pip install -U delocate==0.13.0 packaging==25.0 | |
| setup/download_universal2_wheels.sh | |
| - name: Install dependencies and Crystal (universal2 architecture) | |
| if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build_dist_artifact == 'true' || github.event.inputs.build_arch_macos == 'universal2') }} | |
| timeout-minutes: 2 # normally takes 15s, as of 2025-08-02 | |
| run: | | |
| # Install dependencies | |
| pip install --no-deps --force-reinstall .uwheels/*.whl .uwheels/*.tar.gz | |
| # Install Crystal module as editable | |
| pip install --no-deps -e . | |
| # Install the "crystal" script entry point | |
| poetry install --only-root | |
| - name: Install dependencies and Crystal (native architecture) | |
| if: ${{ github.event_name != 'workflow_dispatch' || !(github.event.inputs.build_dist_artifact == 'true' || github.event.inputs.build_arch_macos == 'universal2') }} | |
| # If build takes a very long time, then it's likely that the version | |
| # of wxPython installed does not offer a precompiled wheel for this | |
| # version of Python. Check the wxPython PyPI page to confirm. | |
| timeout-minutes: 2 # normally takes 6s, as of 2023-03-03 | |
| run: poetry install | |
| - name: Determine how to run python in virtual environment | |
| run: | | |
| # Determine if we should use poetry run prefix | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && ("${{ github.event.inputs.build_dist_artifact }}" == "true" || "${{ github.event.inputs.build_arch_macos }}" == "universal2") ]]; then | |
| echo "POETRY_RUN=" >> $GITHUB_ENV | |
| else | |
| echo "POETRY_RUN=poetry run " >> $GITHUB_ENV | |
| fi | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/Library/Caches/ms-playwright | |
| key: playwright-browsers-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} | |
| - name: Install Playwright browsers | |
| run: ${POETRY_RUN}playwright install chromium | |
| - name: Display SQLite version and JSON support | |
| run: | | |
| ${POETRY_RUN}python -c "import sqlite3; print('SQLite %s' % sqlite3.sqlite_version)" | |
| ${POETRY_RUN}python -c "from crystal.util.xsqlite3 import sqlite_has_json_support; print('JSON Support: ' + ('yes' if sqlite_has_json_support else 'NO'))" | |
| - name: Run non-UI tests | |
| run: ${POETRY_RUN}python -m pytest | |
| - name: Import Developer ID certificate | |
| if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_dist_artifact == 'true' }} | |
| env: | |
| # A base64-encoded .p12 file containing the Developer ID certificate | |
| # and private key, which is used to sign the macOS app and disk image. | |
| # Generate with: base64 < developerID_application.p12 | pbcopy | |
| CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }} | |
| # Password that the Developer ID certificate .p12 file was created with. | |
| CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} | |
| run: | | |
| if [ -z "$CERTIFICATE_P12" ] || [ -z "$CERTIFICATE_PASSWORD" ]; then | |
| echo "WARNING: Codesigning environment variables not set. Skipping codesigning." | |
| if [ "$GITHUB_ACTIONS" = "true" ]; then | |
| echo "::warning::Codesigning environment variables not set. Skipping codesigning." | |
| fi | |
| exit 0 | |
| fi | |
| echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12 | |
| # Create a new keychain with an empty password for temporary use in CI | |
| security create-keychain -p "" build.keychain | |
| # Import the Developer ID certificate and private key into the new keychain, allowing only codesign to access the key | |
| security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign > /dev/null | |
| # Set the list of keychains where credentials are searched by default to be only the new keychain | |
| security list-keychains -s build.keychain | |
| # Set the keychain where codesign stores new credentials | |
| security default-keychain -s build.keychain | |
| # Unlock the keychain so it can be used without prompting for a password | |
| security unlock-keychain -p "" build.keychain | |
| # Grant codesign and other Apple tools the ability to access the private key | |
| # in the unlocked keychain without prompting for a password | |
| security set-key-partition-list -S apple-tool:,apple: -k "" build.keychain > /dev/null | |
| - name: Build .app and disk image | |
| id: build_app_and_disk_image | |
| working-directory: "./setup" | |
| env: | |
| # ex: "Developer ID Application: John Smith (##########)" | |
| CERTIFICATE_NAME: ${{ secrets.CERTIFICATE_NAME }} | |
| # ex: "me@example.com" | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| # ex: "##########" (10-digit Team ID) | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| # An app-specific password created at https://appleid.apple.com/ | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| # Pass the workflow_dispatch input to the script | |
| SHOW_NOTARIZATION_LOG: ${{ github.event.inputs.show_notarization_log }} | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.build_dist_artifact }}" == "true" ]]; then | |
| # If building a distribution artifact then also build the disk image | |
| MAKE_POST_ARGS="" | |
| else | |
| # --app-only: | |
| # 1. Don't build the disk image because it intermittently fails | |
| # with "hdiutil create failed - Resource busy" in CI. | |
| # 2. Don't build the disk image because it takes a long time. | |
| MAKE_POST_ARGS="--app-only" | |
| fi | |
| ${POETRY_RUN}./make-mac.sh $MAKE_POST_ARGS | |
| - name: Verify universal2 build | |
| if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build_dist_artifact == 'true' || github.event.inputs.build_arch_macos == 'universal2') }} | |
| working-directory: "./setup" | |
| run: | | |
| ./verify_universal2.sh dist/Crystal.app | |
| # Upload py2app logs on .app build failure | |
| - name: Upload py2app logs | |
| if: failure() && steps.build_app_and_disk_image.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: py2app-logs-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: | | |
| setup/py2app.stderr.log | |
| setup/py2app.stdout.log | |
| if-no-files-found: ignore | |
| - name: Enable core dumps | |
| if: env.ENABLE_CORE_DUMPS == 'true' | |
| run: | | |
| sudo chmod 1777 /cores | |
| - name: Run UI tests | |
| id: run_ui_tests | |
| env: | |
| CRYSTAL_FAULTHANDLER: 'True' | |
| # Force Crystal to print stdout and stderr rather than sending them to log files | |
| TERM: __interactive__ | |
| CRYSTAL_EXCLUDE_TESTS_MARKED: 'slow' | |
| run: | | |
| ulimit -c unlimited # allow core dumps of unlimited size | |
| CRYSTAL_SCREENSHOTS_DIRPATH=$GITHUB_WORKSPACE/screenshots \ | |
| poetry run -- python3 .github/workflows/watchdog.py --timeout=120 -- \ | |
| "setup/dist/Crystal.app/Contents/MacOS/Crystal" test --parallel ${TEST_NAMES} | |
| - name: Upload screenshot if test failure | |
| if: failure() && steps.run_ui_tests.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: screenshots-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: screenshots | |
| if-no-files-found: ignore | |
| # If test failure then upload a core dump and the related Python binaries | |
| # inside GitHub Action's "hosted tool cache" | |
| - name: "Core dump: Upload if test failure" | |
| id: upload_core_dump | |
| if: env.ENABLE_CORE_DUMPS == 'true' && failure() && steps.run_ui_tests.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coredump-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: /cores | |
| if-no-files-found: error | |
| continue-on-error: true | |
| - name: "Core dump: Archive tool cache" | |
| if: env.ENABLE_CORE_DUMPS == 'true' && failure() && steps.upload_core_dump.outcome == 'success' | |
| run: | | |
| cd "${{ runner.tool_cache }}/Python" | |
| tar -czvf "${{ runner.temp }}/tool_cache_python.tar.gz" * | |
| - name: "Core dump: Upload tool cache artifact" | |
| if: env.ENABLE_CORE_DUMPS == 'true' && failure() && steps.upload_core_dump.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: tool_cache-python-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: ${{runner.temp}}/tool_cache_python.tar.gz | |
| compression-level: 0 # no compression | |
| - name: "Core dump: Upload app artifact" | |
| if: env.ENABLE_CORE_DUMPS == 'true' && failure() && steps.upload_core_dump.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: setup-dist-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: setup/dist | |
| # NOTE: Must remove the --app-only option from make-mac.sh above to | |
| # reinstate build of *.dmg disk image | |
| - name: Upload distribution artifact | |
| if: ${{ github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.build_dist_artifact == 'true' && | |
| matrix.python-version == '3.13.5' && | |
| matrix.os == 'macos-14' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-mac | |
| path: setup/dist-mac/*.dmg | |
| if-no-files-found: warn | |
| build-linux: | |
| strategy: | |
| matrix: | |
| os: | |
| # NOTE: Earliest Linux supported by Crystal is Ubuntu 22.04 (from the README) | |
| # NOTE: When adding new Linux versions, you may need | |
| # to compile new wxPython .wgn files | |
| - ubuntu-22.04 | |
| # Test earliest to latest Python versions supported by Crystal, | |
| # on at least one OS. | |
| python-version: | |
| # NOTE: When adding new Python versions, you may need | |
| # to compile new wxPython .wgn files | |
| - "3.11.9" | |
| - "3.12.9" | |
| - "3.13.5" | |
| - "3.14.0" | |
| fail-fast: false | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 18 # 150% of normal time: 12 min, as of 2025-09-17 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # NOTE: Step must be after "Set up Python" so that Poetry installs | |
| # itself into that Python | |
| - name: Install Poetry | |
| run: | | |
| python -m pip install -U pip setuptools | |
| python -m pip install -U "poetry==2.1.1" | |
| - name: Update APT packages | |
| run: sudo apt-get update | |
| # HACK: Suppress warning "Error retrieving accessibility bus | |
| # address: org.freedesktop.DBus.Error.ServiceUnknown: | |
| # The name org.a11y.Bus was not provided by any .service files" | |
| # when running tests later | |
| - name: Install at-spi2-core | |
| run: sudo apt-get install -y at-spi2-core | |
| # NOTE: Needed for screenshot support while running tests | |
| # TODO: Is scrot actually still needed? gnome-screenshot definitely is. | |
| - name: Install scrot and gnome-screenshot | |
| run: sudo apt-get install scrot gnome-screenshot | |
| - name: Install wxPython dependencies | |
| run: sudo apt-get install -y libgtk-3-dev | |
| # Install additional dependencies for Python 3.13+ wxPython | |
| - name: Install wxPython dependencies for Python 3.13+ | |
| if: ${{ startsWith(matrix.python-version, '3.13.') || startsWith(matrix.python-version, '3.14.') }} | |
| run: sudo apt-get install -y libnotify4 | |
| - name: Install wagon | |
| run: poetry run pip3 install wagon | |
| - name: Patch wagon 1.0.3 for Python 3.14+ | |
| if: startsWith(matrix.python-version, '3.14.') | |
| run: | | |
| WAGON_PATH=$(poetry run python -c 'import importlib.util; spec = importlib.util.find_spec("wagon"); print(spec.origin)') | |
| sed -i 's/from urllib.request import URLopener/from urllib.request import urlopen/' $WAGON_PATH | |
| sed -i 's/from urllib import urlopen/from urllib.request import urlopen/' $WAGON_PATH | |
| # Install wxPython from precompiled wagon because installing | |
| # wxPython from source takes about 40 minutes on GitHub Actions | |
| # | |
| # NOTE: To recompile the .wgn, see instructions in: doc/how_to_make_wxpython_wagon.md | |
| - name: Install dependency wxPython from wagon (Python 3.11) | |
| if: startsWith(matrix.python-version, '3.11.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py311-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Install dependency wxPython from wagon (Python 3.12) | |
| if: startsWith(matrix.python-version, '3.12.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py312-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Install dependency wxPython from wagon (Python 3.13) | |
| if: startsWith(matrix.python-version, '3.13.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py313-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Install dependency wxPython from wagon (Python 3.14) | |
| if: startsWith(matrix.python-version, '3.14.') | |
| run: | | |
| wget --no-verbose https://github.com/davidfstr/Crystal-Web-Archiver/releases/download/v1.5.0b/wxPython-4.2.4-py314-none-linux_x86_64.wgn | |
| poetry run wagon install *.wgn | |
| rm *.wgn | |
| - name: Fail if wxPython wagon is not available for this Python version | |
| if: ${{ !startsWith(matrix.python-version, '3.11.') && !startsWith(matrix.python-version, '3.12.') && !startsWith(matrix.python-version, '3.13.') && !startsWith(matrix.python-version, '3.14.') }} | |
| run: | | |
| echo "ERROR: No precompiled wxPython wagon is available for Python ${{ matrix.python-version }} on Linux." | |
| echo "You must build a .wgn for this Python version and update the workflow." | |
| exit 1 | |
| - name: Install remaining dependencies with Poetry | |
| # If build takes a very long time, the precompiled .wgn in the | |
| # previous build step may no longer be installing the correct | |
| # dependency versions that are consistent with pyproject.toml and | |
| # poetry.lock. In that case you'll need to rebuild the .wgn using | |
| # instructions from: doc/how_to_make_wxpython_wagon.md | |
| timeout-minutes: 2 # normally takes 6s, as of 2023-03-03 | |
| run: poetry install | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-browsers-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} | |
| - name: Install Playwright browsers | |
| run: poetry run playwright install chromium | |
| - name: Display SQLite version and JSON support | |
| run: | | |
| poetry run python -c "import sqlite3; print('SQLite %s' % sqlite3.sqlite_version)" | |
| poetry run python -c "from crystal.util.xsqlite3 import sqlite_has_json_support; print('JSON Support: ' + ('yes' if sqlite_has_json_support else 'NO'))" | |
| - name: Run non-UI tests | |
| run: poetry run python -m pytest | |
| - name: Run UI tests | |
| id: run_ui_tests | |
| env: | |
| CRYSTAL_FAULTHANDLER: 'True' | |
| # Enable a version of malloc() that looks for heap corruption specially: | |
| # https://stackoverflow.com/a/3718867/604063 | |
| MALLOC_CHECK_: '2' | |
| CRYSTAL_EXCLUDE_TESTS_MARKED: '' # INCLUDE 'slow' tests | |
| run: | | |
| CRYSTAL_SCREENSHOTS_DIRPATH=$GITHUB_WORKSPACE/screenshots \ | |
| poetry run -- python3 .github/workflows/watchdog.py --timeout=120 -- \ | |
| poetry run xvfb-run \ | |
| crystal test --parallel ${TEST_NAMES} | |
| - name: Upload screenshot if test failure | |
| if: failure() && steps.run_ui_tests.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: screenshots-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: screenshots | |
| if-no-files-found: ignore | |
| build-windows: | |
| strategy: | |
| matrix: | |
| os: | |
| # NOTE: Earliest Windows supported by Crystal is Windows 11 (from the README) | |
| - windows-2025 # based on Windows 11 version 24H2 (Germanium) | |
| # Test the latest supported Python version only | |
| python-version: | |
| # TODO: Upgrade to Python 3.14 | |
| # NOTE: py2exe 0.14.0.0 only supports Python 3.11, 3.12, and 3.13. | |
| - "3.13.5" | |
| fail-fast: false | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 # 150% of normal time: 20 min, as of 2025-11-29 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # NOTE: Step must be after "Set up Python" so that Poetry installs | |
| # itself into that Python | |
| - name: Install Poetry | |
| run: | | |
| python -m pip install -U pip setuptools | |
| python -m pip install -U "poetry==2.1.1" | |
| - name: Install dependencies with Poetry | |
| # If build takes a very long time, then it's likely that the version | |
| # of wxPython installed does not offer a precompiled wheel for this | |
| # version of Python. Check the wxPython PyPI page to confirm. | |
| timeout-minutes: 2 # normally takes 15s, as of 2023-03-03 | |
| run: poetry install | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~\AppData\Local\ms-playwright | |
| key: playwright-browsers-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} | |
| - name: Install Playwright browsers | |
| run: poetry run playwright install chromium | |
| - name: Display SQLite version and JSON support | |
| run: | | |
| poetry run python -c "import sqlite3; print('SQLite %s' % sqlite3.sqlite_version)" | |
| poetry run python -c "from crystal.util.xsqlite3 import sqlite_has_json_support; print('JSON Support: ' + ('yes' if sqlite_has_json_support else 'NO'))" | |
| - name: Run non-UI tests | |
| run: poetry run python -m pytest | |
| - name: Install Inno Setup 6 | |
| run: | | |
| Invoke-WebRequest -Uri "https://files.jrsoftware.org/is/6/innosetup-6.4.3.exe" -OutFile "$env:USERPROFILE\\is.exe" | |
| Start-Process -Wait -FilePath "$env:USERPROFILE\\is.exe" -ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-" | |
| - name: Display Inno Setup in Program Files | |
| run: | | |
| dir "C:\Program Files (x86)" | |
| dir "C:\Program Files (x86)\Inno Setup 6" | |
| - name: Build .exe and installer | |
| working-directory: ".\\setup" | |
| run: "powershell -File .\\make-win.ps1" | |
| - name: Run UI tests | |
| id: run_ui_tests | |
| working-directory: ".\\setup" | |
| env: | |
| PYTHONUTF8: '1' | |
| run: | | |
| $env:CRYSTAL_SCREENSHOTS_DIRPATH = "$env:GITHUB_WORKSPACE\screenshots" | |
| $env:CRYSTAL_FAULTHANDLER = "True" | |
| $env:CRYSTAL_EXCLUDE_TESTS_MARKED = "slow" | |
| poetry run -- python3 ..\.github\workflows\watchdog.py --timeout=120 -- ` | |
| poetry run python run_exe.py ` | |
| "dist\Crystal.exe" "---" "test" ${env:TEST_NAMES} | |
| - name: Upload screenshot if test failure | |
| if: failure() && steps.run_ui_tests.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: screenshots-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: screenshots | |
| if-no-files-found: ignore | |
| - name: Upload distribution artifact | |
| if: ${{ github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.build_dist_artifact == 'true' && | |
| matrix.python-version == '3.13.5' && | |
| matrix.os == 'windows-2025' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-win | |
| path: "setup\\dist-win\\*.exe" | |
| if-no-files-found: warn |