Files
kleinanzeigen-bot/.github/workflows/build.yml
Jens 33d1964f86 feat: speed up and stabilise test suite (#676)
## ℹ️ Description
*Provide a concise summary of the changes introduced in this pull
request.*

- Link to the related issue(s): Issue #
- Describe the motivation and context for this change.

Refactors the test harness for faster and more reliable feedback: adds
deterministic time freezing for update checks, accelerates and refactors
smoke tests to run in-process, defaults pytest to xdist with durations
tracking, and adjusts CI triggers so PRs run the test matrix only once.

## 📋 Changes Summary

- add pytest-xdist + durations reporting defaults, force deterministic
locale and slow markers, and document the workflow adjustments
- run smoke tests in-process (no subprocess churn), mock update
checks/logging, and mark slow specs appropriately
- deflake update check interval tests by freezing datetime and simplify
FixedDateTime helper
- limit GitHub Actions `push` trigger to `main` so feature branches rely
on the single pull_request run

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x]  New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


##  Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Ensure tests run in a consistent English locale and restore prior
locale after each run
  * Mark integration scraping tests as slow for clearer categorization
* Replace subprocess-based CLI tests with an in-process runner that
returns structured results and captures combined stdout/stderr/logs;
disable update checks during smoke tests
* Freeze current time in update-check tests for deterministic assertions
* Add mock for process enumeration in web‑scraping unit tests to
stabilize macOS-specific warnings

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-12 21:29:51 +01:00

529 lines
16 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot
#
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
name: Build
on: # https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows
schedule:
# https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule
- cron: '0 15 1 * *'
push:
branches:
- main
tags-ignore: # don't build tags
- '**'
paths-ignore:
- '**/*.md'
- '.act*'
- '.editorconfig'
- '.git*'
- '.github/*.yml'
- '.github/ISSUE_TEMPLATE/*'
- '.github/workflows/codeql-analysis.yml'
- '.github/workflows/stale.yml'
- '.github/workflows/update-python-deps.yml'
- '.github/workflows/validate-pr.yml'
- 'codecov.yml'
pull_request:
paths-ignore:
- '**/*.md'
- '.act*'
- '.editorconfig'
- '.git*'
- '.github/*.yml'
- '.github/ISSUE_TEMPLATE/*'
- '.github/workflows/codeql-analysis.yml'
- '.github/workflows/stale.yml'
- '.github/workflows/update-python-deps.yml'
- '.github/workflows/validate-pr.yml'
- 'codecov.yml'
workflow_dispatch:
# https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_dispatch
defaults:
run:
shell: bash
jobs:
###########################################################
build:
###########################################################
if: github.event_name != 'push' || github.ref == 'refs/heads/main'
permissions:
packages: write
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel # X86
PYTHON_VERSION: "3.10"
PUBLISH_RELEASE: false
- os: macos-latest # ARM
PYTHON_VERSION: "3.10"
PUBLISH_RELEASE: false
- os: ubuntu-latest
PYTHON_VERSION: "3.10"
PUBLISH_RELEASE: false
- os: windows-latest
PYTHON_VERSION: "3.10"
PUBLISH_RELEASE: false
- os: macos-15-intel # X86
PYTHON_VERSION: "3.14"
PUBLISH_RELEASE: true
- os: macos-latest # ARM
PYTHON_VERSION: "3.14"
PUBLISH_RELEASE: true
- os: ubuntu-latest
PYTHON_VERSION: "3.14"
PUBLISH_RELEASE: true
- os: windows-latest
PYTHON_VERSION: "3.14"
PUBLISH_RELEASE: true
runs-on: ${{ matrix.os }} # https://github.com/actions/runner-images#available-images
timeout-minutes: 20
steps:
- name: "Show: GitHub context"
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: printf '%s' "$GITHUB_CONTEXT" | python -m json.tool
- name: "Show: environment variables"
run: env | sort
- name: Configure Fast APT Mirror
uses: vegardit/fast-apt-mirror.sh@v1
- name: Git Checkout
uses: actions/checkout@v5 # https://github.com/actions/checkout
- name: "Install: Chromium Browser"
if: env.ACT == 'true' && startsWith(matrix.os, 'ubuntu')
run: |
if ! hash google-chrome &>/dev/null; then
curl -sSfL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o /tmp/chrome.deb
sudo dpkg -i /tmp/chrome.deb || true
sudo apt-get --no-install-recommends -y --fix-broken install
fi
- name: "Install: Python and PDM" # https://github.com/pdm-project/setup-pdm
uses: pdm-project/setup-pdm@v4
with:
python-version: "${{ matrix.PYTHON_VERSION }}"
cache: ${{ !startsWith(matrix.os, 'macos') }} # https://github.com/pdm-project/setup-pdm/issues/55
- name: "Install: Python dependencies"
run: |
set -eux
python --version
python -m pip install --upgrade pip
pip install --upgrade pdm
if [[ ! -e .venv ]]; then
pdm venv create || true
fi
pdm sync --clean -v
- name: Display project metadata
run: pdm show
- name: Check with pip-audit
# until https://github.com/astral-sh/ruff/issues/8277
run:
pdm run pip-audit --progress-spinner off --skip-editable --verbose
- name: Check with ruff
run: pdm run ruff check
- name: Check with mypy
run: pdm run mypy
- name: Check with basedpyright
run: pdm run basedpyright
- name: Run unit tests
run: pdm run utest:cov -vv
- name: Run integration tests
run: |
set -eux
case "${{ matrix.os }}" in
ubuntu-*)
sudo apt-get install --no-install-recommends -y xvfb
xvfb-run pdm run itest:cov -vv
;;
*) pdm run itest:cov -vv
;;
esac
- name: Run smoke tests
run: pdm run smoke:cov -vv
- name: Run app from source
run: |
echo "
login:
username: 'john.doe@example.com'
password: 'such_a_secret'
" > config.yaml
set -eux
pdm run app help
pdm run app version
pdm run app verify
- name: "Install: binutils (strip)"
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get install --no-install-recommends -y binutils
- name: "Install: UPX"
if: startsWith(matrix.os, 'windows')
run: |
set -eu
upx_download_url=$(curl -fsSL -H "Authorization: token ${{ github.token }}" https://api.github.com/repos/upx/upx/releases/latest | grep browser_download_url | grep win64.zip | cut "-d\"" -f4)
echo "Downloading [$upx_download_url]..."
curl -fL -o /tmp/upx.zip $upx_download_url
echo "Extracting upx zip..."
mkdir /tmp/upx
7z e /tmp/upx.zip -o/tmp/upx *.exe -r
echo "$(cygpath -wa /tmp/upx)" >> $GITHUB_PATH
/tmp/upx/upx.exe --version
- name: Build self-contained executable
run: |
set -eux
if [[ "${{ runner.os }}" == "Windows" ]]; then
NO_UPX=1 pdm run compile
mv dist/kleinanzeigen-bot.exe dist/kleinanzeigen-bot-uncompressed.exe
fi
pdm run compile
ls -l dist
- name: Run self-contained executable
run: |
set -eux
dist/kleinanzeigen-bot help
dist/kleinanzeigen-bot version
dist/kleinanzeigen-bot verify
- name: Upload self-contained executable
uses: actions/upload-artifact@v5
if: (github.ref_name == 'main' || github.ref_name == 'release') && matrix.PUBLISH_RELEASE && !env.ACT
with:
name: artifacts-${{ matrix.os }}
path: dist/kleinanzeigen-bot*
- name: Build Docker image
if: startsWith(matrix.os, 'ubuntu')
run: |
set -eux
bash docker/build-image.sh
docker run --rm second-hand-friends/kleinanzeigen-bot help
- name: Publish Docker image
if: github.repository_owner == 'Second-Hand-Friends' && github.ref_name == 'main' && matrix.PUBLISH_RELEASE && startsWith(matrix.os, 'ubuntu') && !env.ACT
run: |
set -eux
echo "${{ github.token }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
image_name="second-hand-friends/kleinanzeigen-bot"
docker image tag $image_name ghcr.io/$image_name
docker push ghcr.io/$image_name
- name: Collect coverage reports
uses: actions/upload-artifact@v5
if: (github.ref_name == 'main' || github.event_name == 'pull_request') && !env.ACT
with:
name: coverage-${{ matrix.os }}-py${{ matrix.PYTHON_VERSION }}
include-hidden-files: true
path: .temp/coverage-*.xml
if-no-files-found: error
###########################################################
publish-coverage:
###########################################################
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 5
if: (github.ref_name == 'main' || github.event_name == 'pull_request') && !github.event.act
steps:
- name: Git Checkout # required to avoid https://docs.codecov.com/docs/error-reference#unusable-reports
uses: actions/checkout@v5 # https://github.com/actions/checkout
- name: Download coverage reports
uses: actions/download-artifact@v6
with:
pattern: coverage-*
path: coverage
- name: List coverage reports
run: find . -name coverage-*.xml
- name: Publish unit-test coverage
uses: codecov/codecov-action@v5 # https://github.com/codecov/codecov-action
with:
slug: ${{ github.repository }}
name: unit-coverage
flags: unit-tests
disable_search: true
files: coverage/**/coverage-unit.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Publish integration-test coverage
uses: codecov/codecov-action@v5 # https://github.com/codecov/codecov-action
with:
slug: ${{ github.repository }}
name: integration-coverage
flags: integration-tests
disable_search: true
files: coverage/**/coverage-integration.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Publish smoke-test coverage
uses: codecov/codecov-action@v5 # https://github.com/codecov/codecov-action
with:
slug: ${{ github.repository }}
name: smoke-coverage
flags: smoke-tests
disable_search: true
files: coverage/**/coverage-smoke.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
###########################################################
publish-release:
###########################################################
needs: [build, publish-coverage]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # to delete/create GitHub releases
packages: write # to delete untagged docker images
# run on 'main' and 'release' branch when:
# build succeeded, AND
# publish-coverage succeeded OR was skipped
if: >
always()
&& needs.build.result == 'success'
&& (needs.publish-coverage.result == 'success' || needs.publish-coverage.result == 'skipped')
&& (github.ref_name == 'main' || github.ref_name == 'release') && !github.event.act
concurrency: publish-${{ github.ref_name }}-release # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idconcurrency
steps:
- name: "Show: GitHub context"
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo $GITHUB_CONTEXT
- name: "Show: environment variables"
run: env | sort
- name: Configure Fast APT Mirror
uses: vegardit/fast-apt-mirror.sh@v1
- name: Git Checkout
# only required by "gh release create" to prevent "fatal: Not a git repository"
uses: actions/checkout@v5 # https://github.com/actions/checkout
- name: Delete untagged docker image
continue-on-error: true
uses: actions/delete-package-versions@v5
with:
token: ${{ github.token }}
delete-only-untagged-versions: true
package-name: kleinanzeigen-bot
package-type: container
- name: Download build artifacts
uses: actions/download-artifact@v6
- name: Rename build artifacts
run: |
mv artifacts-macos-15-intel/kleinanzeigen-bot kleinanzeigen-bot-darwin-amd64
mv artifacts-macos-latest/kleinanzeigen-bot kleinanzeigen-bot-darwin-arm64
mv artifacts-ubuntu-latest/kleinanzeigen-bot kleinanzeigen-bot-linux-amd64
mv artifacts-windows-latest/kleinanzeigen-bot-uncompressed.exe kleinanzeigen-bot-windows-amd64-uncompressed.exe
mv artifacts-windows-latest/kleinanzeigen-bot.exe kleinanzeigen-bot-windows-amd64.exe
- name: Install ClamAV
run: |
sudo apt-get update
sudo apt-get install -y clamav
sudo systemctl stop clamav-freshclam.service
sudo freshclam
- name: Scan build artifacts
run: clamscan kleinanzeigen-*
- name: "Determine release name"
id: release
if: github.event_name != 'schedule'
run: |
case "$GITHUB_REF_NAME" in
main)
echo "name=preview" >>"$GITHUB_OUTPUT"
;;
release)
echo "name=latest" >>"$GITHUB_OUTPUT"
;;
esac
- name: "Delete previous '${{ steps.release.outputs.name }}' release"
if: steps.release.outputs.name && steps.release.outputs.name != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: ${{ steps.release.outputs.name }}
# https://cli.github.com/manual/gh_release_delete
run: |
GH_DEBUG=1 gh release delete "$RELEASE_NAME" --yes --cleanup-tag || true
- name: "Create '${{ steps.release.outputs.name }}' Release"
if: steps.release.outputs.name && steps.release.outputs.name != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: ${{ steps.release.outputs.name }}
COMMIT_MSG: | # https://stackoverflow.com/a/78420438/5116073
${{ github.event.head_commit.message }}
---
#### ⚠️ Rechtlicher Hinweis
<p>Die Verwendung dieses Programms kann unter Umständen gegen die zum jeweiligen Zeitpunkt bei kleinanzeigen.de geltenden Nutzungsbedingungen verstoßen.
Es liegt in Ihrer Verantwortung, die rechtliche Zulässigkeit der Nutzung dieses Programms zu prüfen.
Die Entwickler übernehmen keinerlei Haftung für mögliche Schäden oder rechtliche Konsequenzen.
Die Nutzung erfolgt auf eigenes Risiko. Jede rechtswidrige Verwendung ist untersagt.</p>
#### ⚠️ Legal notice
<p>The use of this program could violate the terms of service of kleinanzeigen.de valid at the time of use.
It is your responsibility to ensure the legal compliance of its use.
The developers assume no liability for any damages or legal consequences.
Use is at your own risk. Any unlawful use is strictly prohibited.</p>
# https://cli.github.com/manual/gh_release_create
run: |
GH_DEBUG=1 gh release create "$RELEASE_NAME" \
--title "$RELEASE_NAME" \
${{ steps.release.outputs.name == 'latest' && '--latest' || '' }} \
${{ steps.release.outputs.name == 'preview' && '--prerelease' || '' }} \
--notes "$COMMIT_MSG" \
--target "${{ github.sha }}" \
kleinanzeigen-bot-darwin-amd64 \
kleinanzeigen-bot-darwin-arm64 \
kleinanzeigen-bot-linux-amd64 \
kleinanzeigen-bot-windows-amd64.exe \
kleinanzeigen-bot-windows-amd64-uncompressed.exe
- name: "Delete intermediate build artifacts"
uses: geekyeggo/delete-artifact@v5 # https://github.com/GeekyEggo/delete-artifact/
with:
name: "*"
failOnError: false
###########################################################
dependabot-pr-auto-merge:
###########################################################
needs: build
if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: write
steps:
- name: Merge Dependabot PR
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
PR_URL: ${{github.event.pull_request.html_url}}
run: gh pr merge --auto --rebase "$PR_URL"
###########################################################
pdm-pr-auto-merge:
###########################################################
needs: build
if: github.event_name == 'pull_request' && github.actor == 'kleinanzeigen-bot-tu[bot]' && github.head_ref == 'dependencies/pdm'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: write
steps:
- name: Merge Dependabot PR
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
PR_URL: ${{github.event.pull_request.html_url}}
run: gh pr merge --auto --rebase "$PR_URL"