borg/.github/workflows/ci.yml
TW 59a33c6ef6
Merge pull request #10249 from ThomasWaldmann/ci-cpa-1.5.0-haiku-beta6
CI: cross-platform-actions 1.5.0, re-add haiku (r1beta6)
2026-08-30 21:10:54 +02:00

945 lines
39 KiB
YAML

# badge: https://github.com/borgbackup/borg/workflows/CI/badge.svg?branch=master
name: CI
on:
push:
branches: [ master ]
tags:
- '2.*'
pull_request:
branches: [ master ]
paths:
- '**.py'
- '**.pyx'
- '**.c'
- '**.h'
- '**.yml'
- '**.toml'
- '**.cfg'
- '**.ini'
- 'requirements.d/*'
- '!docs/**'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
# Force colored tox and pytest output even without a tty - the GitHub
# Actions log viewer renders ANSI colors. tox passes both vars through
# to pytest (pass_env = ["*"]).
PY_COLORS: "1" # pytest
TOX_COLORED: "yes" # tox's own output
jobs:
lint:
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
security:
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit[toml]
- name: Run Bandit
run: |
bandit -r src/borg -c pyproject.toml
asan_ubsan:
runs-on: ubuntu-26.04
timeout-minutes: 25
needs: [lint]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Just fetching one commit is not enough for setuptools-scm, so we fetch all.
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get install -y pkg-config build-essential
sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.d/development.lock.txt
- name: Build Borg with ASan/UBSan
# Build the C/Cython extensions with AddressSanitizer and UndefinedBehaviorSanitizer enabled.
# How this works:
# - The -fsanitize=address,undefined flags inject runtime checks into our native code. If a bug is hit
# (e.g., buffer overflow, use-after-free, out-of-bounds, or undefined behavior), the sanitizer prints
# a detailed error report to stderr, including a stack trace, and forces the process to exit with
# non-zero status. In CI, this will fail the step/job so you will notice.
# - ASAN_OPTIONS/UBSAN_OPTIONS configure the sanitizers' runtime behavior (see below for meanings).
env:
CFLAGS: "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined"
CXXFLAGS: "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined"
LDFLAGS: "-fsanitize=address,undefined"
# ASAN_OPTIONS controls AddressSanitizer runtime tweaks:
# - detect_leaks=0: Disable LeakSanitizer to avoid false positives with CPython/pymalloc in short-lived tests.
# - strict_string_checks=1: Make invalid string operations (e.g., over-reads) more likely to be detected.
# - check_initialization_order=1: Catch uses that depend on static initialization order (C++).
# - detect_stack_use_after_return=1: Detect stack-use-after-return via stack poisoning (may increase overhead).
ASAN_OPTIONS: "detect_leaks=0:strict_string_checks=1:check_initialization_order=1:detect_stack_use_after_return=1"
# UBSAN_OPTIONS controls UndefinedBehaviorSanitizer runtime:
# - print_stacktrace=1: Include a stack trace for UB reports to ease debugging.
# Note: UBSan is recoverable by default (process may continue after reporting). If you want CI to
# abort immediately and fail on the first UB, add `halt_on_error=1` (e.g., UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1").
UBSAN_OPTIONS: "print_stacktrace=1"
# PYTHONDEVMODE enables additional Python runtime checks and warnings.
PYTHONDEVMODE: "1"
run: pip install -e .
- name: Run tests under sanitizers
env:
ASAN_OPTIONS: "detect_leaks=0:strict_string_checks=1:check_initialization_order=1:detect_stack_use_after_return=1"
UBSAN_OPTIONS: "print_stacktrace=1"
PYTHONDEVMODE: "1"
# Ensure the ASan runtime is loaded first to avoid "ASan runtime does not come first" warnings.
# We discover libasan/libubsan paths via gcc and preload them for the Python test process.
# the remote tests are slow and likely won't find anything useful
run: |
set -euo pipefail
export LD_PRELOAD="$(gcc -print-file-name=libasan.so):$(gcc -print-file-name=libubsan.so)"
echo "Using LD_PRELOAD=$LD_PRELOAD"
# the sanitizer runtimes write their reports to stderr, so they still show up per worker
pytest -v -n auto --benchmark-skip -k "not remote"
native_tests:
needs: [lint]
permissions:
contents: read
id-token: write
attestations: write
strategy:
# entries with "allow-failure": true (e.g. prerelease pythons) do not fail
# the matrix, see continue-on-error below.
fail-fast: true
# noinspection YAMLSchemaValidation
# "coverage": "1" collects coverage in that entry - we only do that for the
# oldest and the newest supported python, measuring is not free, see #9470.
matrix: >-
${{ fromJSON(
github.event_name == 'pull_request' && '{
"include": [
{"os": "ubuntu-26.04", "python-version": "3.11", "toxenv": "mypy"},
{"os": "ubuntu-26.04", "python-version": "3.11", "toxenv": "docs"},
{"os": "ubuntu-26.04", "python-version": "3.11", "toxenv": "py311-llfuse", "coverage": "1"},
{"os": "ubuntu-26.04", "python-version": "3.12", "toxenv": "py312-pyfuse3", "store_cache": "1"},
{"os": "ubuntu-26.04", "python-version": "3.14", "toxenv": "py314-mfusepy", "coverage": "1"},
{"os": "ubuntu-26.04", "python-version": "3.15-dev", "toxenv": "py315-mfusepy", "allow-failure": true}
]
}' || '{
"include": [
{"os": "ubuntu-26.04", "python-version": "3.11", "toxenv": "py311-llfuse", "coverage": "1"},
{"os": "ubuntu-26.04", "python-version": "3.12", "toxenv": "py312-pyfuse3"},
{"os": "ubuntu-26.04", "python-version": "3.13", "toxenv": "py313-mfusepy", "store_cache": "1"},
{"os": "ubuntu-26.04", "python-version": "3.14", "toxenv": "py314-pyfuse3", "coverage": "1", "binary": "borg-linux-glibc243-x86_64-gh"},
{"os": "ubuntu-26.04-arm", "python-version": "3.14", "toxenv": "py314-pyfuse3", "coverage": "1", "binary": "borg-linux-glibc243-arm64-gh"},
{"os": "macos-15", "python-version": "3.14", "toxenv": "py314-none", "coverage": "1", "binary": "borg-macos-15-arm64-gh"},
{"os": "macos-15-intel", "python-version": "3.14", "toxenv": "py314-none", "coverage": "1", "binary": "borg-macos-15-x86_64-gh"},
{"os": "ubuntu-26.04", "python-version": "3.15-dev", "toxenv": "py315-mfusepy", "allow-failure": true}
]
}'
) }}
env:
TOXENV: ${{ matrix.toxenv }}
runs-on: ${{ matrix.os }}
# Python 3.15 is not released yet, so do not fail the whole matrix if it (or a
# dependency not having wheels / not compiling for it yet) breaks.
# Remove the "allow-failure" matrix entries once 3.15 is final.
continue-on-error: ${{ matrix.allow-failure || false }}
# macOS machines can be slow, if overloaded.
timeout-minutes: 360
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Just fetching one commit is not enough for setuptools-scm, so we fetch all.
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip
# Not on tags: that is when the release binaries are built and attested,
# and they should not be able to pick up a poisoned cache entry.
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pip
key: ${{ runner.os }}-${{ runner.arch }}-pip-${{ hashFiles('requirements.d/development.lock.txt') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-pip-
- name: Cache tox environments
# Not on tags: that is when the release binaries are built and attested,
# and they should not be able to pick up a poisoned cache entry.
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .tox
key: ${{ runner.os }}-${{ runner.arch }}-tox-${{ matrix.toxenv }}-${{ hashFiles('requirements.d/development.lock.txt', 'pyproject.toml') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-tox-${{ matrix.toxenv }}-
${{ runner.os }}-${{ runner.arch }}-tox-
- name: Install Linux packages
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt-get update
sudo apt-get install -y pkg-config build-essential
sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev
sudo apt-get install -y bash zsh fish # for shell completion tests
sudo apt-get install -y rclone openssh-server curl
if [[ "$TOXENV" == *"llfuse"* ]]; then
sudo apt-get install -y libfuse-dev fuse # Required for Python llfuse module
elif [[ "$TOXENV" == *"pyfuse3"* || "$TOXENV" == *"mfusepy"* ]]; then
sudo apt-get install -y libfuse3-dev fuse3 # Required for Python pyfuse3 module
fi
- name: Install macOS packages
if: ${{ runner.os == 'macOS' }}
run: |
brew unlink pkg-config@0.29.2 || true
brew bundle install
# the runner's unresolvable .local hostname takes macOS ~35s to give up on,
# which borg paid per spawned process (import-time fqdn) - hours of test time,
# see #9470. .local is mDNS territory, an /etc/hosts entry alone does not help.
sudo scutil --set HostName borg-ci-mac
echo "127.0.0.1 borg-ci-mac" | sudo tee -a /etc/hosts
- name: Configure OpenSSH SFTP server (test only)
if: ${{ runner.os == 'Linux' && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}
run: |
sudo mkdir -p /run/sshd
sudo useradd -m -s /bin/bash sftpuser || true
# Create SSH key for the CI user and authorize it for sftpuser
mkdir -p ~/.ssh
chmod 700 ~/.ssh
test -f ~/.ssh/id_ed25519 || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
sudo mkdir -p /home/sftpuser/.ssh
sudo chmod 700 /home/sftpuser/.ssh
sudo cp ~/.ssh/id_ed25519.pub /home/sftpuser/.ssh/authorized_keys
sudo chown -R sftpuser:sftpuser /home/sftpuser/.ssh
sudo chmod 600 /home/sftpuser/.ssh/authorized_keys
# Allow publickey auth and enable Subsystem sftp
sudo sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
if ! grep -q '^Subsystem sftp' /etc/ssh/sshd_config; then echo 'Subsystem sftp /usr/lib/openssh/sftp-server' | sudo tee -a /etc/ssh/sshd_config; fi
# Ensure host keys exist to avoid slow generation on first sshd start
sudo ssh-keygen -A
# Start sshd (listen on default 22 inside runner)
sudo /usr/sbin/sshd -D &
# Add host key to known_hosts so paramiko trusts it
ssh-keyscan -H localhost 127.0.0.1 | tee -a ~/.ssh/known_hosts
# Start ssh-agent and add our key so paramiko can use the agent
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# The rest test starts "borg serve --rest" over ssh as sftpuser, which runs the borg
# under test from the tox venv under $HOME. Allow sftpuser to traverse into the runner
# home so it can reach that borg (the venv dirs/files are created world-r/x by tox/pip).
sudo chmod o+x "$HOME"
# Export SFTP test URL for tox via GITHUB_ENV
echo "BORG_TEST_SFTP_REPO=sftp://sftpuser@localhost:22/borg/sftp-repo" >> $GITHUB_ENV
echo "BORG_TEST_REST_REPO=rest://sftpuser@localhost:22/borg/rest-repo" >> $GITHUB_ENV
- name: Install and configure MinIO S3 server (test only)
if: ${{ runner.os == 'Linux' && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}
run: |
set -e
arch=$(uname -m)
case "$arch" in
x86_64|amd64) srv_url=https://dl.min.io/server/minio/release/linux-amd64/minio; cli_url=https://dl.min.io/client/mc/release/linux-amd64/mc ;;
aarch64|arm64) srv_url=https://dl.min.io/server/minio/release/linux-arm64/minio; cli_url=https://dl.min.io/client/mc/release/linux-arm64/mc ;;
*) echo "Unsupported arch: $arch"; exit 1 ;;
esac
curl -fsSL -o /usr/local/bin/minio "$srv_url"
curl -fsSL -o /usr/local/bin/mc "$cli_url"
sudo chmod +x /usr/local/bin/minio /usr/local/bin/mc
export PATH=/usr/local/bin:$PATH
# Start MinIO on :9000 with default credentials (minioadmin/minioadmin)
MINIO_DIR="$GITHUB_WORKSPACE/.minio-data"
MINIO_LOG="$GITHUB_WORKSPACE/.minio.log"
mkdir -p "$MINIO_DIR"
nohup minio server "$MINIO_DIR" --address ":9000" >"$MINIO_LOG" 2>&1 &
# Wait for MinIO port to be ready
for i in $(seq 1 60); do (echo > /dev/tcp/127.0.0.1/9000) >/dev/null 2>&1 && break; sleep 1; done
# Configure client and create bucket
mc alias set local http://127.0.0.1:9000 minioadmin minioadmin
mc mb --ignore-existing local/borg
# Export S3 test URL for tox via GITHUB_ENV
echo "BORG_TEST_S3_REPO=s3:minioadmin:minioadmin@http://127.0.0.1:9000/borg/s3-repo" >> $GITHUB_ENV
- name: Enable store cache (test only)
if: ${{ matrix.store_cache == '1' }}
run: echo "BORG_STORE_CACHE=1" >> $GITHUB_ENV
- name: Install Python requirements
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements.d/development.lock.txt
- name: Install borgbackup
run: |
if [[ "$TOXENV" == *"llfuse"* ]]; then
pip install -ve ".[llfuse,cockpit,s3,sftp,rclone]"
elif [[ "$TOXENV" == *"pyfuse3"* ]]; then
pip install -ve ".[pyfuse3,cockpit,s3,sftp,rclone]"
elif [[ "$TOXENV" == *"mfusepy"* ]]; then
pip install -ve ".[mfusepy,cockpit,s3,sftp,rclone]"
else
pip install -ve ".[cockpit,s3,sftp,rclone]"
fi
- name: Build Borg fat binaries (${{ matrix.binary }})
if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}
run: |
pip install -r requirements.d/pyinstaller.txt
./scripts/build-borg-using-pyinstaller.sh
- name: Smoke-test the built binary (${{ matrix.binary }})
if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}
run: |
pushd dist/binary
echo "single-file binary"
chmod +x borg.exe
./borg.exe -V
echo "single-directory binary"
chmod +x borg-dir/borg.exe
./borg-dir/borg.exe -V
tar czf borg.tgz borg-dir
popd
# Ensure locally built binary in ./dist/binary/borg-dir is found during tests
export PATH="$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH"
echo "borg.exe binary in PATH"
borg.exe -V
- name: Prepare binaries (${{ matrix.binary }})
if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}
env:
BINARY: ${{ matrix.binary }}
run: |
mkdir -p artifacts
if [ -f dist/binary/borg.exe ]; then
cp dist/binary/borg.exe "artifacts/$BINARY"
fi
if [ -f dist/binary/borg.tgz ]; then
cp dist/binary/borg.tgz "artifacts/$BINARY.tgz"
fi
echo "binary files"
ls -l artifacts/
- name: Attest binaries provenance (${{ matrix.binary }})
if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: 'artifacts/*'
- name: Upload binaries (${{ matrix.binary }})
if: ${{ matrix.binary && startsWith(github.ref, 'refs/tags/') }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.binary }}
path: artifacts/*
if-no-files-found: error
- name: run tox env
env:
# tox passes this through to pytest (pass_env = ["*"])
PYTEST_ADDOPTS: ${{ !matrix.coverage && '--no-cov' || '' }}
run: |
# do not use fakeroot, but run as root. avoids the dreaded EISDIR sporadic failures. see #2482.
#sudo -E bash -c "tox -e py"
# Ensure locally built binary in ./dist/binary/borg-dir is found during tests
export PATH="$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH"
tox --skip-missing-interpreters
- name: Upload test results to Codecov
if: ${{ !cancelled() && !contains(matrix.toxenv, 'mypy') && !contains(matrix.toxenv, 'docs') }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
OS: ${{ runner.os }}
python: ${{ matrix.python-version }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
env_vars: OS,python
files: test-results.xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() && matrix.coverage }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
OS: ${{ runner.os }}
python: ${{ matrix.python-version }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: coverage
env_vars: OS,python
files: coverage.xml
vm_tests:
permissions:
contents: read
id-token: write
attestations: write
runs-on: ubuntu-26.04
timeout-minutes: 180
needs: [lint]
continue-on-error: true
strategy:
fail-fast: false
matrix:
# pyver: python the test step installs; part of the pip cache key.
# 3.14+ gets coverage's near-zero-overhead sysmon core, see #9470.
include:
- os: freebsd
version: '15.1'
display_name: FreeBSD
pyver: '3.14'
# Controls binary build and provenance attestation on tags
do_binaries: true
artifact_prefix: borg-freebsd-15-x86_64-gh
- os: netbsd
version: '11.0'
display_name: NetBSD
pyver: '3.14'
do_binaries: false
- os: openbsd
version: '7.9'
display_name: OpenBSD
pyver: '3.13'
do_binaries: false
# extra RAM for the mfs-backed TMPDIR, see the openbsd test step
memory: 12G
- os: omnios
version: 'r151056'
display_name: OmniOS
pyver: '3.13'
do_binaries: false
- os: haiku
version: 'r1beta6'
display_name: Haiku
pyver: '3.14'
do_binaries: false
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false
# .pip-cache lives inside the workspace, which cross-platform-actions
# rsyncs into the VM and back, so wheels built from sdists in one run
# (most of the VM setup time) are reused by the next one.
- name: Cache pip-built wheels
# Not on tags: that is when the release binaries are built and attested,
# and they should not be able to pick up a poisoned cache entry.
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .pip-cache
key: ${{ matrix.os }}-${{ matrix.version }}-py${{ matrix.pyver }}-pip-${{ hashFiles('requirements.d/development.lock.txt') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.version }}-py${{ matrix.pyver }}-pip-
- name: Start VM for ${{ matrix.display_name }}
id: cross_os
# a normal boot takes < 1 minute; since 1.4.0 the action bounds
# waiting for boot itself, this is just an additional safety net.
timeout-minutes: 15
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
with:
operating_system: ${{ matrix.os }}
version: ${{ matrix.version }}
shell: bash
# the default VM size (2 cpus) leaves half of the runner's 4 vcpus idle
cpu_count: 4
memory: ${{ matrix.memory || '8G' }}
- name: Test on ${{ matrix.display_name }}
shell: cpa.sh {0}
env:
DO_BINARIES: ${{ matrix.do_binaries }}
run: |
set -euxo pipefail
# a hung test must fail instead of stalling the suite until the
# job timeout (tox passes all env vars through to pytest).
export PYTEST_TIMEOUT=300
# colored tox/pytest output - the workflow-level env vars do not
# reach into the VM, so export them here again.
export PY_COLORS=1
export TOX_COLORED=yes
# see the "Cache pip-built wheels" step
export PIP_CACHE_DIR="$PWD/.pip-cache"
case "${{ matrix.os }}" in
freebsd)
export IGNORE_OSVERSION=yes
sudo -E pkg update -f
sudo -E pkg install -y liblz4 pkgconf
sudo -E pkg install -y fusefs-libs
sudo -E kldload fusefs
sudo -E sysctl vfs.usermount=1
sudo -E chmod 666 /dev/fuse
# enable POSIX.1e ACLs on the UFS root fs (covers /tmp), so the ACL tests do not skip (#9144)
sudo mount -u -o acls /
mount | grep ' / ' | grep -w acls
sudo -E pkg install -y rust
sudo -E pkg install -y gmake
sudo -E pkg install -y git
sudo -E pkg install -y python314 py314-sqlite3
sudo ln -sf /usr/local/bin/python3.14 /usr/local/bin/python3
sudo ln -sf /usr/local/bin/python3.14 /usr/local/bin/python
sudo ln -sf /usr/local/bin/pip3.14 /usr/local/bin/pip3
sudo ln -sf /usr/local/bin/pip3.14 /usr/local/bin/pip
# required for libsodium/pynacl build
export MAKE=gmake
python -m venv .venv
. .venv/bin/activate
python -V
pip -V
python -m pip install --upgrade pip wheel
pip install -r requirements.d/development.lock.txt
pip install -e ".[mfusepy,cockpit,s3,sftp,rclone]"
if [[ "${{ matrix.do_binaries }}" == "true" && "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then
python -m pip install -r requirements.d/pyinstaller.txt
./scripts/build-borg-using-pyinstaller.sh
pushd dist/binary
echo "single-file binary"
chmod +x borg.exe
./borg.exe -V
echo "single-directory binary"
chmod +x borg-dir/borg.exe
./borg-dir/borg.exe -V
tar czf borg.tgz borg-dir
popd
mkdir -p artifacts
if [ -f dist/binary/borg.exe ]; then
cp -v dist/binary/borg.exe artifacts/${{ matrix.artifact_prefix }}
fi
if [ -f dist/binary/borg.tgz ]; then
cp -v dist/binary/borg.tgz artifacts/${{ matrix.artifact_prefix }}.tgz
fi
fi
export PATH="$(pwd)/dist/binary:$PATH"
tox -e py314-mfusepy
;;
netbsd)
arch="$(uname -m)"
sudo -E mkdir -p /usr/pkg/etc/pkgin
echo "https://ftp.NetBSD.org/pub/pkgsrc/packages/NetBSD/${arch}/11.0/All" | sudo tee /usr/pkg/etc/pkgin/repositories.conf > /dev/null
sudo -E pkgin update
sudo -E pkgin -y upgrade
sudo -E pkgin -y install lz4 git
sudo -E pkgin -y install rust
sudo -E pkgin -y install pkg-config
# python 3.14 for coverage's fast sysmon core, see #9470
sudo -E pkgin -y install py314-pip py314-virtualenv py314-tox
sudo -E ln -sf /usr/pkg/bin/python3.14 /usr/pkg/bin/python3
sudo -E ln -sf /usr/pkg/bin/pip3.14 /usr/pkg/bin/pip3
sudo -E ln -sf /usr/pkg/bin/virtualenv-3.14 /usr/pkg/bin/virtualenv3
sudo -E ln -sf /usr/pkg/bin/tox-3.14 /usr/pkg/bin/tox3
# Ensure base system admin tools are on PATH for the non-root shell
export PATH="/sbin:/usr/sbin:$PATH"
# On the netbsd 11 VM, / is a fs with extended attributes.
export TMPDIR="/tmp_eafs"
sudo -E mkdir -p ${TMPDIR}
sudo -E chmod 1777 ${TMPDIR}
touch ${TMPDIR}/testfile
lsextattr user ${TMPDIR}/testfile && echo "[xattr] *** xattrs SUPPORTED on ${TMPDIR}! ***"
tox3 -e py314-none
;;
openbsd)
# Put the temp tree (pip/cc build temps, pytest tmp dirs and the
# borg test repos in them) on a memory-backed filesystem instead
# of the slow FFS disk. Sized generously (the pytest tmp tree is
# only cleaned up after the run); the VM gets 12G RAM for this.
sudo mkdir -p /mfs
# -O2: FFS2 format - mfs defaults to FFS1, whose 32 bit
# timestamps silently wrap (breaks test_extract_y2261).
sudo mount_mfs -O2 -s 6g swap /mfs
sudo chmod 1777 /mfs
export TMPDIR=/mfs
# 7.9 has no python 3.14 pkg yet - move to 3.14 + update pyver when it ships (#9470)
sudo -E pkg_add lz4 git rust openssl%3.5 py3-pip py3-virtualenv py3-tox
export BORG_OPENSSL_NAME=eopenssl35
tox -e py313-none
;;
omnios)
# r151056 has no python-314 pkg yet - move to 3.14 + update pyver when it ships (#9470)
sudo pkg install gcc14 git pkg-config python-313 gnu-make gnu-coreutils rust
sudo ln -sf /usr/bin/python3.13 /usr/bin/python3
sudo ln -sf /usr/bin/python3.13-config /usr/bin/python3-config
sudo python3 -m ensurepip
sudo python3 -m pip install virtualenv
# On omniOS /tmp is swap-backed tmpfs (small, RAM-bound), so the pip/cargo
# build temps and the pytest temp tree quickly exhaust it ("no space left on
# device"). /var/tmp is disk-backed (ZFS), so redirect TMPDIR there.
export TMPDIR=/var/tmp/borg-ci
mkdir -p "$TMPDIR"
# show whether xattrs work on the ZFS-backed TMPDIR (they are files in a
# hidden per-file attribute directory there, listed via runat(1))
touch "$TMPDIR/testfile"
/usr/bin/runat "$TMPDIR/testfile" ls -a && echo "*** xattrs supported on $TMPDIR ***"
rm "$TMPDIR/testfile"
python3 -m venv .venv
. .venv/bin/activate
python -V
pip -V
python -m pip install --upgrade pip wheel
pip install -r requirements.d/development.lock.txt
# no fuse support on omnios in our tests usually
pip install -e .
tox -e py313-none
;;
haiku)
pkgman refresh
# already installed:
# pkgman install -y git pkgconfig lz4 openssl3
pkgman install -y lz4_devel openssl3_devel
pkgman install -y rust_bin
python3 -m ensurepip --upgrade
python3 -m pip install --upgrade pip wheel
python3 -m venv .venv
. .venv/bin/activate
export PKG_CONFIG_PATH="/system/develop/lib/pkgconfig:/system/lib/pkgconfig:${PKG_CONFIG_PATH:-}"
export BORG_LIBLZ4_PREFIX=/system/develop
export BORG_OPENSSL_PREFIX=/system/develop
# this VM corrupts data every now and then: rustc crashed on crate metadata it
# had written itself ("assertion failed: bytes[len] == STR_SENTINEL") and pip
# found sha256 mismatches reading big wheels back from its own cache. Build
# outside /boot/system/cache/tmp (haiku's /tmp) and run one rustc at a time,
# in case that corruption comes from disk or memory pressure.
export TMPDIR=/boot/home/borg-tmp
mkdir -p "$TMPDIR"
export CARGO_TARGET_DIR="$TMPDIR/cargo-target"
export CARGO_BUILD_JOBS=1
# retry a pip install that hit a corrupted file, dropping the cached (possibly
# corrupted) files first, so one of them does not fail the whole job - and does
# not end up in the actions cache, failing every following run, too.
pip_install() {
for attempt in 1 2 3; do
pip install "$@" && return 0
echo "*** pip install failed (attempt $attempt), purging the pip cache and retrying ***"
pip cache purge || true
done
return 1
}
pip_install -r requirements.d/development.lock.txt
pip_install -e .
# troubles with either tox or pytest xdist, so we run pytest manually:
pytest -v -n auto -rs --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml --benchmark-skip -k "not remote and not socket"
;;
esac
- name: Upload artifacts
if: startsWith(github.ref, 'refs/tags/') && matrix.do_binaries
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.artifact_prefix }}
path: artifacts/*
if-no-files-found: ignore
- name: Attest provenance
if: startsWith(github.ref, 'refs/tags/') && matrix.do_binaries
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: 'artifacts/*'
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
OS: ${{ matrix.os }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
env_vars: OS
files: test-results.xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
OS: ${{ matrix.os }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: coverage
env_vars: OS
files: coverage.xml
windows_tests:
if: true # can be used to temporarily disable the build
runs-on: windows-latest
timeout-minutes: 90
needs: [lint]
permissions:
contents: read
id-token: write
attestations: write
env:
MSYS2_ARG_CONV_EXCL: "*"
MSYS2_ENV_CONV_EXCL: "*"
# see the "Cache pip-built wheels" step. MSYS2_ENV_CONV_EXCL above keeps
# this a Windows path when it enters the msys2 shell.
PIP_CACHE_DIR: ${{ github.workspace }}\.pip-cache
defaults:
run:
shell: msys2 {0}
steps:
# actions/checkout runs Git for Windows, whose system config sets
# core.autocrlf=true, while the msys2 git the build steps below use does
# not: it would see every text file as modified, so setuptools-scm called
# the checkout dirty and gave the binaries a guessed-next .devN version
# instead of the tag version, see #10199. .gitattributes takes care of
# this as well, this is here so that it also works for older tags.
- name: Do not convert line endings on checkout
shell: pwsh
run: git config --global core.autocrlf false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
# MSYS2's mingw Python cannot use PyPI's win_amd64 wheels, so pip builds
# all compiled deps from source - incl. building maturin via cargo, just
# to build the blake3 wheel. Persist the wheels pip builds.
- name: Cache pip-built wheels
# Not on tags: that is when the release binaries are built and attested,
# and they should not be able to pick up a poisoned cache entry.
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .pip-cache
key: windows-msys2-pip-${{ hashFiles('pyproject.toml', 'requirements.d/pyinstaller.txt') }}
restore-keys: |
windows-msys2-pip-
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0
with:
msystem: UCRT64
update: true
- name: Install system packages
run: ./scripts/msys2-install-deps development
- name: Build python venv
run: |
# building native extensions in the venv fails, so we try to use the system packages
python -m venv --system-site-packages env
. env/bin/activate
# python -m pip install --upgrade pip
# pip install --upgrade setuptools build wheel
pip install -r requirements.d/pyinstaller.txt
- name: Build
run: |
# build borg.exe
. env/bin/activate
pip install -e ".[cockpit,s3,sftp,rclone]"
./scripts/build-borg-using-pyinstaller.sh
# build sdist and wheel in dist/...
python -m build
# Same layout and naming as the binaries of the other platforms, so that
# the release job picks this up as a release asset, too. The single-file
# binary keeps its .exe extension - Windows needs it to run the file.
- name: Prepare binaries (borg-windows-x86_64-gh)
run: |
pushd dist/binary
echo "single-file binary"
./borg.exe -V
echo "single-directory binary"
./borg-dir/borg.exe -V
tar czf borg.tgz borg-dir
popd
mkdir -p artifacts
cp dist/binary/borg.exe artifacts/borg-windows-x86_64-gh.exe
cp dist/binary/borg.tgz artifacts/borg-windows-x86_64-gh.tgz
echo "binary files"
ls -l artifacts/
- name: Attest binaries provenance (borg-windows-x86_64-gh)
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: 'artifacts/*'
- name: Upload binaries (borg-windows-x86_64-gh)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: borg-windows-x86_64-gh
path: artifacts/*
if-no-files-found: error
- name: Run tests
run: |
# Ensure locally built binary in ./dist/binary/borg-dir is found during tests
export PATH="$GITHUB_WORKSPACE/dist/binary/borg-dir:$PATH"
borg.exe -V
. env/bin/activate
python -m pytest -n4 --benchmark-skip -vv -rs -k "not remote" --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
OS: ${{ runner.os }}
python: '3.11'
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
env_vars: OS,python
files: test-results.xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
OS: ${{ runner.os }}
python: '3.11'
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: coverage
env_vars: OS,python
files: coverage.xml
release:
# Build the sdist and draft the GitHub release with the binaries the jobs
# above built for this tag, see .github/workflows/release.yml.
#
# vm_tests is continue-on-error (a flaky BSD VM must not stop a release), so
# this waits for it, but only requires native_tests to have succeeded. A
# binary that did not get built is warned about while drafting the release.
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/') && needs.native_tests.result == 'success' }}
needs: [native_tests, vm_tests]
permissions:
contents: write
id-token: write
attestations: write
uses: ./.github/workflows/release.yml
pypi:
# Upload the sdist the release job built to PyPI.
#
# This job can not live in release.yml with the rest of the release code:
# PyPI trusted publishing does not work from a reusable workflow, see
# https://docs.pypi.org/trusted-publishers/troubleshooting/
#
# One-time setup, so that no API token has to be stored anywhere:
# - on pypi.org, add a trusted publisher to the "borgbackup" project:
# owner "borgbackup", repository "borg", workflow "ci.yml",
# environment "pypi".
# - create the "pypi" environment in the repository settings. Configuring
# required reviewers for it makes this (irreversible) upload wait for an
# approval, which is the last chance to stop a release.
if: ${{ startsWith(github.ref, 'refs/tags/') && needs.release.result == 'success' }}
needs: [release]
runs-on: ubuntu-26.04
timeout-minutes: 30
environment:
name: pypi
url: https://pypi.org/project/borgbackup/
permissions:
contents: read
id-token: write # trusted publishing
steps:
- name: Get the sdist built by the release job
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sdist
path: dist
- name: What we are about to upload
run: ls -l dist/
- name: Upload to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2