Skip to content

Manual CI run

Manual CI run #840

Workflow file for this run

# 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