From 5495af1c9e60924308974826ab19c7be5fc69df5 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Sun, 2 Aug 2026 10:09:30 -0400 Subject: [PATCH 1/9] Add tests/extra/ framework for CI-only regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formalise the "extra" tests that CI runs on top of `git annex test` and DataLad's own battery, so new real-world-regression checks can be added by dropping in a file rather than editing a workflow step. Two parallel prototype implementations are shipped side-by-side (pytest and bats) to compare styles before picking one. New under tests/extra/: - README.md documents the layout, per-test skip semantics, and local invocation for both frameworks. - pytest/ (test_dynlibs.py, test_url_backend.py) and bats/ (helpers.bash, setup_suite.bash, dynlibs.bats, url_backend.bats). - Shared pytest helpers + version-reporting hook live in tests/conftest.py (rather than tests/extra/pytest/conftest.py) so `pytest_report_header` fires regardless of the caller's argument path — pytest loads ancestor conftests eagerly but descendants lazily during collection, too late for the header. Tests: - dynlibs: strace-based guard against libpcre ENOENT-lookup regressions during `git-annex version` / `git-annex init`. Linux-only (auto- skipped where strace is absent). Replaces the inline "Seek of dynlibs" step of the previous test-annex-more job, verbatim, in both frameworks. - url_backend: clones the real repronim/ReproTube DataLad dataset with --no-single-branch (need the git-annex branch for URL-key metadata), asserts `git annex whereis` decodes the URL-encoded key back to `youtube.com/watch?v=`, and `git annex get` retrieves the file (~18 MB, served over HTTPS by the origin remote, so no yt-dlp required). The `get` variant is xfailed (strict=False) below the fix version so old builds do not red the run but a fixed-build regression fails loudly. Bats has no xfail primitive; it uses `skip` as the nearest equivalent (documented in-file). - Fix version 10.20260420 identified from upstream commit 8fd9b67ed8 "factor out extendUrlWithPath and use for git http remote key urls" (Joey Hess, 2026-02-16), first shipped in that release. The bug originally motivating this test is a URL-encoded key path (from `URL--yt&chttps&c%%…` produced by keyFile) yielding an invalid URI when concatenated with a git http remote base — see also the con/git-annex-side patch 20260212-43a3f3aaf2 that shipped the fix ahead of upstream (removed once absorbed upstream). Version-reporting hook (mirrors the dandi-schema pytest_plugin pattern): - pytest prints an "extra-tests tools" line with `git`, `bats`, `yt-dlp`, `youtube-dl`, `strace` first-lines (or `(missing)`); a `platform:` line; the critical fields from `git annex version` (git-annex has no `--json` for version, so the human output is parsed); and a "git-annex releases in this repo newer than installed (VER): N" line derived from `git tag --list "10.*"`. - bats prints the same header via setup_suite.bash -> print_versions in helpers.bash, using bats fd 3 so it's visible without opting into --show-output-of-passing-tests. Workflow template: - `test-annex-more` (Ubuntu/macOS-only, single inline "Seek of dynlibs" step) is replaced by `test-extra`, which runs on all four platforms (Ubuntu, macOS Intel, macOS ARM64, Windows). On each platform it installs the platform-appropriate bats (apt / brew / git-clone), sets up Python 3.12 for pytest, then runs both `python -m pytest -v tests/extra/pytest/` and `bats tests/extra/bats/`. Regenerated the four concrete build-*.yaml files via the existing mkworkflows.py. - REUSE.toml gets `tests/**` added to the MIT/DataLad-Team default block so `reuse lint` stays clean. The bats variant will be eliminated in a follow-up commit once the comparison has been reviewed; kept here so the review is against tree, not against a diff. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- .github/workflows/build-macos-arm64.yaml | 53 +++-- .github/workflows/build-macos.yaml | 53 +++-- .github/workflows/build-ubuntu.yaml | 53 +++-- .github/workflows/build-windows.yaml | 78 ++++++- .../template/build-{{ostype}}.yaml.j2 | 53 +++-- REUSE.toml | 1 + tests/.gitignore | 2 + tests/conftest.py | 195 ++++++++++++++++++ tests/extra/README.md | 39 ++++ tests/extra/bats/dynlibs.bats | 39 ++++ tests/extra/bats/helpers.bash | 119 +++++++++++ tests/extra/bats/setup_suite.bash | 8 + tests/extra/bats/url_backend.bats | 58 ++++++ tests/extra/pytest/test_dynlibs.py | 54 +++++ tests/extra/pytest/test_url_backend.py | 107 ++++++++++ 15 files changed, 835 insertions(+), 77 deletions(-) create mode 100644 tests/.gitignore create mode 100644 tests/conftest.py create mode 100644 tests/extra/README.md create mode 100644 tests/extra/bats/dynlibs.bats create mode 100644 tests/extra/bats/helpers.bash create mode 100644 tests/extra/bats/setup_suite.bash create mode 100644 tests/extra/bats/url_backend.bats create mode 100644 tests/extra/pytest/test_dynlibs.py create mode 100644 tests/extra/pytest/test_url_backend.py diff --git a/.github/workflows/build-macos-arm64.yaml b/.github/workflows/build-macos-arm64.yaml index dab3b92c98..60d35fdcf9 100644 --- a/.github/workflows/build-macos-arm64.yaml +++ b/.github/workflows/build-macos-arm64.yaml @@ -134,7 +134,7 @@ jobs: git-annex_"${{ steps.build-version.outputs.version }}".dmg - name: Upload packages - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: git-annex-macos-arm64-dmg_${{ steps.build-version.outputs.version }} path: | @@ -182,7 +182,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -223,7 +223,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-macos-arm64-dmg_${{ needs.build-package.outputs.build-version }} @@ -324,7 +324,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -340,7 +340,11 @@ jobs: See for more information. - test-annex-more: + test-extra: + # Runs the extra-tests suite in tests/extra/ (both pytest and bats + # variants). Each test declares its own skip conditions, so the + # default is: run on every platform, skip only where an individual + # test's dependencies aren't available (e.g. dynlibs needs strace). runs-on: macos-15 needs: build-package steps: @@ -353,13 +357,13 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ macOS ARM64 \ - test-annex-more \ + test-extra \ pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-macos-arm64-dmg_${{ needs.build-package.outputs.build-version }} @@ -370,17 +374,27 @@ jobs: hdiutil detach /Volumes/git-annex/ echo /Applications/git-annex.app/Contents/MacOS >> "$GITHUB_PATH" - - name: Seek of dynlibs + - name: Install bats + run: brew install bats-core + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install pytest + run: python -m pip install --upgrade pip pytest + + - name: Configure git identity run: | - mkdir /tmp/testrepo; cd /tmp/testrepo; git init - function nfailed() { - strace -f git-annex "$1" 2>&1 | awk "/$2.*ENOENT/{print}" | tee /dev/fd/2 | wc -l - } - # We should get some reasonable number (not 40) of directories look up for dynamic libraries - liblookups= - PS4='> '; set -x - test $(nfailed version "libpcre.*so") -lt 7 - test $(nfailed init "libpcre.*so") -lt 260 + git config --global user.email "test@github.land" + git config --global user.name "GitHub Almighty" + + - name: Run pytest suite + run: python -m pytest -v tests/extra/pytest/ + + - name: Run bats suite + run: bats tests/extra/bats/ - name: Set final PR status if: always() && github.event.inputs.pr != '' @@ -388,11 +402,12 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ macOS ARM64 \ - test-annex-more \ + test-extra \ "${{ job.status }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + test-datalad: runs-on: macos-15 needs: build-package @@ -416,7 +431,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-macos-arm64-dmg_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/build-macos.yaml b/.github/workflows/build-macos.yaml index f0c9659ade..49823ab0d6 100644 --- a/.github/workflows/build-macos.yaml +++ b/.github/workflows/build-macos.yaml @@ -134,7 +134,7 @@ jobs: git-annex_"${{ steps.build-version.outputs.version }}".dmg - name: Upload packages - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: git-annex-macos-dmg_${{ steps.build-version.outputs.version }} path: | @@ -182,7 +182,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -222,7 +222,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-macos-dmg_${{ needs.build-package.outputs.build-version }} @@ -323,7 +323,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -339,7 +339,11 @@ jobs: See for more information. - test-annex-more: + test-extra: + # Runs the extra-tests suite in tests/extra/ (both pytest and bats + # variants). Each test declares its own skip conditions, so the + # default is: run on every platform, skip only where an individual + # test's dependencies aren't available (e.g. dynlibs needs strace). runs-on: macos-15-intel needs: build-package steps: @@ -352,13 +356,13 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ macOS \ - test-annex-more \ + test-extra \ pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-macos-dmg_${{ needs.build-package.outputs.build-version }} @@ -369,17 +373,27 @@ jobs: hdiutil detach /Volumes/git-annex/ echo /Applications/git-annex.app/Contents/MacOS >> "$GITHUB_PATH" - - name: Seek of dynlibs + - name: Install bats + run: brew install bats-core + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install pytest + run: python -m pip install --upgrade pip pytest + + - name: Configure git identity run: | - mkdir /tmp/testrepo; cd /tmp/testrepo; git init - function nfailed() { - strace -f git-annex "$1" 2>&1 | awk "/$2.*ENOENT/{print}" | tee /dev/fd/2 | wc -l - } - # We should get some reasonable number (not 40) of directories look up for dynamic libraries - liblookups= - PS4='> '; set -x - test $(nfailed version "libpcre.*so") -lt 7 - test $(nfailed init "libpcre.*so") -lt 260 + git config --global user.email "test@github.land" + git config --global user.name "GitHub Almighty" + + - name: Run pytest suite + run: python -m pytest -v tests/extra/pytest/ + + - name: Run bats suite + run: bats tests/extra/bats/ - name: Set final PR status if: always() && github.event.inputs.pr != '' @@ -387,11 +401,12 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ macOS \ - test-annex-more \ + test-extra \ "${{ job.status }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + test-datalad: runs-on: macos-15-intel needs: build-package @@ -415,7 +430,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-macos-dmg_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/build-ubuntu.yaml b/.github/workflows/build-ubuntu.yaml index b8ee4daf27..3cadc48d96 100644 --- a/.github/workflows/build-ubuntu.yaml +++ b/.github/workflows/build-ubuntu.yaml @@ -142,7 +142,7 @@ jobs: if: "!contains(env.DEB_BUILD_OPTIONS, 'nocheck')" - name: Upload packages - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: git-annex-debianstandalone-packages_${{ steps.build-version.outputs.version }} path: | @@ -226,7 +226,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -266,7 +266,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-debianstandalone-packages_${{ needs.build-package.outputs.build-version }} @@ -361,7 +361,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -377,7 +377,11 @@ jobs: See for more information. - test-annex-more: + test-extra: + # Runs the extra-tests suite in tests/extra/ (both pytest and bats + # variants). Each test declares its own skip conditions, so the + # default is: run on every platform, skip only where an individual + # test's dependencies aren't available (e.g. dynlibs needs strace). runs-on: ubuntu-24.04 needs: build-package steps: @@ -390,13 +394,13 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ Ubuntu \ - test-annex-more \ + test-extra \ pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-debianstandalone-packages_${{ needs.build-package.outputs.build-version }} @@ -404,17 +408,27 @@ jobs: run: | sudo dpkg -i git-annex*.deb - - name: Seek of dynlibs + - name: Install strace and bats + run: sudo apt-get update -qq && sudo apt-get install -y strace bats + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install pytest + run: python -m pip install --upgrade pip pytest + + - name: Configure git identity run: | - mkdir /tmp/testrepo; cd /tmp/testrepo; git init - function nfailed() { - strace -f git-annex "$1" 2>&1 | awk "/$2.*ENOENT/{print}" | tee /dev/fd/2 | wc -l - } - # We should get some reasonable number (not 40) of directories look up for dynamic libraries - liblookups= - PS4='> '; set -x - test $(nfailed version "libpcre.*so") -lt 7 - test $(nfailed init "libpcre.*so") -lt 260 + git config --global user.email "test@github.land" + git config --global user.name "GitHub Almighty" + + - name: Run pytest suite + run: python -m pytest -v tests/extra/pytest/ + + - name: Run bats suite + run: bats tests/extra/bats/ - name: Set final PR status if: always() && github.event.inputs.pr != '' @@ -422,11 +436,12 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ Ubuntu \ - test-annex-more \ + test-extra \ "${{ job.status }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + test-datalad: runs-on: ubuntu-24.04 needs: build-package @@ -450,7 +465,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-debianstandalone-packages_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/build-windows.yaml b/.github/workflows/build-windows.yaml index e7e259a44f..a80fe0b8ea 100644 --- a/.github/workflows/build-windows.yaml +++ b/.github/workflows/build-windows.yaml @@ -154,7 +154,7 @@ jobs: git-annex-installer_"${{ steps.build-version.outputs.version }}".exe - name: Upload packages - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: git-annex-windows-installer_${{ steps.build-version.outputs.version }} path: | @@ -202,7 +202,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -242,7 +242,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-windows-installer_${{ needs.build-package.outputs.build-version }} @@ -309,7 +309,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c + uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -325,6 +325,74 @@ jobs: See for more information. + test-extra: + # Runs the extra-tests suite in tests/extra/ (both pytest and bats + # variants). Each test declares its own skip conditions, so the + # default is: run on every platform, skip only where an individual + # test's dependencies aren't available (e.g. dynlibs needs strace). + runs-on: windows-2025 + needs: build-package + steps: + - name: Checkout this repository + uses: actions/checkout@v6 + + - name: Create pending PR status + if: github.event.inputs.pr != '' + run: | + .github/workflows/tools/set-pr-status \ + "${{ github.event.inputs.pr }}" \ + Windows \ + test-extra \ + pending + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download git-annex package + uses: actions/download-artifact@v7 + with: + name: git-annex-windows-installer_${{ needs.build-package.outputs.build-version }} + + - name: Install git-annex package + shell: powershell + run: | + ./git-annex-installer_*.exe /S + + - name: Install bats + run: | + git clone --depth=1 https://github.com/bats-core/bats-core.git "$RUNNER_TEMP/bats-core" + echo "$RUNNER_TEMP/bats-core/bin" >> "$GITHUB_PATH" + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install pytest + run: python -m pip install --upgrade pip pytest + + - name: Configure git identity + run: | + git config --global user.email "test@github.land" + git config --global user.name "GitHub Almighty" + + - name: Run pytest suite + run: python -m pytest -v tests/extra/pytest/ + + - name: Run bats suite + run: bats tests/extra/bats/ + + - name: Set final PR status + if: always() && github.event.inputs.pr != '' + run: | + .github/workflows/tools/set-pr-status \ + "${{ github.event.inputs.pr }}" \ + Windows \ + test-extra \ + "${{ job.status }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + test-datalad: runs-on: windows-2025 needs: build-package @@ -348,7 +416,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: git-annex-windows-installer_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/template/build-{{ostype}}.yaml.j2 b/.github/workflows/template/build-{{ostype}}.yaml.j2 index 8c885f7c62..a0c3417103 100644 --- a/.github/workflows/template/build-{{ostype}}.yaml.j2 +++ b/.github/workflows/template/build-{{ostype}}.yaml.j2 @@ -570,8 +570,11 @@ jobs: See for more information. -{% if ostype == "ubuntu" or ostype.startswith("macos") %} - test-annex-more: + test-extra: + # Runs the extra-tests suite in tests/extra/ (both pytest and bats + # variants). Each test declares its own skip conditions, so the + # default is: run on every platform, skip only where an individual + # test's dependencies aren't available (e.g. dynlibs needs strace). runs-on: {{runs_on}} needs: build-package steps: @@ -584,7 +587,7 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ {{osname}} \ - test-annex-more \ + test-extra \ pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -603,17 +606,37 @@ jobs: {{step}} {% endfor %} - - name: Seek of dynlibs + {% if ostype == "ubuntu" %} + - name: Install strace and bats + run: sudo apt-get update -qq && sudo apt-get install -y strace bats + {% elif ostype.startswith("macos") %} + - name: Install bats + run: brew install bats-core + {% elif ostype == "windows" %} + - name: Install bats + run: | + git clone --depth=1 https://github.com/bats-core/bats-core.git "$RUNNER_TEMP/bats-core" + echo "$RUNNER_TEMP/bats-core/bin" >> "$GITHUB_PATH" + {% endif %} + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install pytest + run: python -m pip install --upgrade pip pytest + + - name: Configure git identity run: | - mkdir /tmp/testrepo; cd /tmp/testrepo; git init - function nfailed() { - strace -f git-annex "$1" 2>&1 | awk "/$2.*ENOENT/{print}" | tee /dev/fd/2 | wc -l - } - # We should get some reasonable number (not 40) of directories look up for dynamic libraries - liblookups= - PS4='> '; set -x - test $(nfailed version "libpcre.*so") -lt 7 - test $(nfailed init "libpcre.*so") -lt 260 + git config --global user.email "test@github.land" + git config --global user.name "GitHub Almighty" + + - name: Run pytest suite + run: python -m pytest -v tests/extra/pytest/ + + - name: Run bats suite + run: bats tests/extra/bats/ - name: Set final PR status if: always() && github.event.inputs.pr != '' @@ -621,12 +644,12 @@ jobs: .github/workflows/tools/set-pr-status \ "${{ github.event.inputs.pr }}" \ {{osname}} \ - test-annex-more \ + test-extra \ "${{ job.status }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -{% endif %} + {% if test_datalad %} test-datalad: runs-on: {{runs_on}} diff --git a/REUSE.toml b/REUSE.toml index 54558d7451..2072c50676 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -13,6 +13,7 @@ path = [ ".github/**", "clients/**", "docs/**", + "tests/**", ] precedence = "aggregate" SPDX-FileCopyrightText = "2020-2026 DataLad Team " diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000000..6c56ff1bd9 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.pytest_cache/ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..d723b041da --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,195 @@ +""" +Shared pytest configuration for the tests/ tree. + +Lives at tests/ (rather than tests/extra/pytest/) so that +`pytest_report_header` fires regardless of whether the caller runs +`pytest tests`, `pytest tests/extra/pytest`, or a single test file. +pytest loads conftest.py files eagerly along the ancestor chain from +each argument path down to rootdir; a conftest below the given path is +loaded lazily during collection, which is too late for the header. +""" + +from __future__ import annotations + +import platform +import shutil +import subprocess +from functools import lru_cache +from pathlib import Path + +import pytest + + +# --- shared fixtures ---------------------------------------------------------- + + +@pytest.fixture +def git_annex_repo(tmp_path: Path) -> Path: + """An empty, initialised git-annex repository in a fresh temp dir.""" + subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True) + subprocess.run( + ["git", "config", "user.email", "test@github.land"], + cwd=tmp_path, check=True, + ) + subprocess.run( + ["git", "config", "user.name", "GitHub Almighty"], + cwd=tmp_path, check=True, + ) + subprocess.run(["git", "annex", "init", "-q"], cwd=tmp_path, check=True) + return tmp_path + + +# --- git-annex version helpers ----------------------------------------------- + + +@lru_cache(maxsize=1) +def _git_annex_version_output() -> str | None: + """Raw stdout of `git annex version`, cached for the session.""" + if shutil.which("git-annex") is None: + return None + try: + return subprocess.run( + ["git", "annex", "version"], + capture_output=True, text=True, timeout=15, check=True, + ).stdout + except (subprocess.SubprocessError, OSError): + return None + + +def git_annex_version() -> str | None: + """ + Bare version string reported by git-annex, e.g. "10.20260421" + (`-g` build suffix stripped). None if git-annex is not + installed / not runnable. + """ + out = _git_annex_version_output() + if not out: + return None + for line in out.splitlines(): + if line.startswith("git-annex version:"): + return line.split(":", 1)[1].strip().split("-", 1)[0] + return None + + +def _version_key(s: str) -> tuple[int, ...]: + return tuple(int(p) for p in s.split(".") if p.isdigit()) + + +def git_annex_version_below(threshold: str) -> bool: + """ + True if the installed git-annex version is *strictly* below `threshold`. + Compares as tuples of ints on the "." separator ("10.20220615" < + "10.20260421"). False if git-annex is missing. + """ + v = git_annex_version() + if v is None: + return False + return _version_key(v) < _version_key(threshold) + + +@lru_cache(maxsize=32) +def git_annex_releases_since(threshold: str) -> int | None: + """ + Count git-annex release tags in the current repository that are + strictly newer than `threshold`. Returns None if not inside a git + repo or if there are no matching tags (e.g. a shallow checkout). + """ + if shutil.which("git") is None: + return None + try: + out = subprocess.run( + ["git", "tag", "--list", "10.*"], + capture_output=True, text=True, timeout=10, check=True, + ).stdout + except (subprocess.SubprocessError, OSError): + return None + key = _version_key(threshold) + n = 0 + for tag in out.splitlines(): + tag = tag.strip() + if not tag: + continue + try: + if _version_key(tag) > key: + n += 1 + except ValueError: + continue + return n if n or out.strip() else None + + +def _git_annex_summary() -> list[str]: + """Critical fields from `git annex version` (no --json upstream).""" + out = _git_annex_version_output() + if out is None: + return ["git-annex: NOT INSTALLED"] + wanted = ( + "git-annex version", + "build flags", + "dependency versions", + "operating system", + "supported repository versions", + ) + lines = [] + for line in out.splitlines(): + key = line.split(":", 1)[0].strip().lower() + if key in wanted: + lines.append(f" {line.strip()}") + return lines + + +def _first_line(cmd: list[str]) -> str | None: + if shutil.which(cmd[0]) is None: + return None + try: + out = subprocess.run( + cmd, capture_output=True, text=True, timeout=15, check=True, + ).stdout + except (subprocess.SubprocessError, OSError): + return None + return out.splitlines()[0].strip() if out.strip() else None + + +# Fix version for the URL-encoded-key bug (upstream 8fd9b67ed8, +# 2026-02-16; first shipped in release 10.20260420). Kept here as a +# single source of truth so tests and the header agree. +URL_BACKEND_FIX_VERSION = "10.20260420" + + +def pytest_report_header(config: pytest.Config) -> list[str]: + """Version + tool info at the top of pytest's session banner.""" + tools = [ + ("git", ["git", "--version"]), + ("bats", ["bats", "--version"]), + ("yt-dlp", ["yt-dlp", "--version"]), + ("youtube-dl", ["youtube-dl", "--version"]), + ("strace", ["strace", "--version"]), + ] + tool_versions = [] + for name, cmd in tools: + first = _first_line(cmd) + tool_versions.append(f"{name}={first if first is not None else '(missing)'}") + + installed = git_annex_version() + if installed is None: + since_line = "git-annex releases newer than installed: (git-annex not installed)" + else: + n_since = git_annex_releases_since(installed) + if n_since is None: + since_line = ( + f"git-annex releases in this repo newer than installed " + f"({installed}): (no tags found)" + ) + else: + since_line = ( + f"git-annex releases in this repo newer than installed " + f"({installed}): {n_since}" + ) + + header = [ + "extra-tests tools: " + ", ".join(tool_versions), + f"platform: {platform.platform()}", + "git-annex:", + *_git_annex_summary(), + since_line, + ] + return header diff --git a/tests/extra/README.md b/tests/extra/README.md new file mode 100644 index 0000000000..0163a913c1 --- /dev/null +++ b/tests/extra/README.md @@ -0,0 +1,39 @@ +# Extra tests + +Tests that CI runs on top of `git annex test` and the DataLad test battery. +Each test targets a specific real-world scenario, often a regression that +was seen in the wild and would slip past both upstream test suites. + +Two parallel implementations of the same suite are kept side-by-side so we +can compare style and eventually pick one to promote (upstream-friendly or +otherwise): + +- `pytest/` — Python + pytest. Fixtures for temp directories and repo + cloning; skip markers are `@pytest.mark.skipif(...)`. +- `bats/` — [Bats](https://bats-core.readthedocs.io/) shell tests. Skip + logic uses the `skip` built-in inside `setup()`. + +CI runs **both**. Each test declares its own skip conditions (missing +`strace`, missing `yt-dlp`, unsupported platform) so the default is: run +everywhere, skip only where the required tool isn't available. + +## Tests + +| Test | Purpose | Platforms | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------- | +| `dynlibs` | Regression guard on the number of failed dynamic-library lookups (`strace -e ENOENT` on `libpcre.*so`) during `git-annex version`/`init`. | Linux | +| `url_backend` | Regression guard for parsing "odd" URL-backend keys (URL-encoded characters like `&c`, `%%`, `,63v` etc.) on a real DataLad dataset. | All | + +## Running locally + +```bash +# pytest suite +python -m pytest -v tests/extra/pytest/ + +# bats suite (needs bats-core installed) +bats tests/extra/bats/ +``` + +Both suites assume `git-annex` is on `PATH`. The `url_backend` test +clones a small (~18 MB) real DataLad dataset from `datasets.datalad.org` +so needs network access. diff --git a/tests/extra/bats/dynlibs.bats b/tests/extra/bats/dynlibs.bats new file mode 100644 index 0000000000..351b036047 --- /dev/null +++ b/tests/extra/bats/dynlibs.bats @@ -0,0 +1,39 @@ +#!/usr/bin/env bats +# +# Guard against regressions in git-annex's dynamic-library lookup behaviour. +# See tests/extra/pytest/test_dynlibs.py for the equivalent pytest version. + +load helpers + +setup() { + require_linux + require_cmd strace + TESTREPO="$(mktemp -d "${BATS_TEST_TMPDIR:-/tmp}/dynlibs.XXXXXX")" + ( cd "$TESTREPO" && git init -q ) +} + +teardown() { + [[ -n "${TESTREPO:-}" && -d "$TESTREPO" ]] && rm -rf "$TESTREPO" + return 0 +} + +nfailed() { + local subcommand=$1 + local pattern=$2 + strace -f git-annex "$subcommand" 2>&1 \ + | awk "/${pattern}.*ENOENT/{print}" \ + | tee /dev/fd/2 \ + | wc -l +} + +@test "libpcre ENOENT lookups on 'git-annex version' < 7" { + cd "$TESTREPO" + n=$(nfailed version "libpcre.*so") + [ "$n" -lt 7 ] +} + +@test "libpcre ENOENT lookups on 'git-annex init' < 260" { + cd "$TESTREPO" + n=$(nfailed init "libpcre.*so") + [ "$n" -lt 260 ] +} diff --git a/tests/extra/bats/helpers.bash b/tests/extra/bats/helpers.bash new file mode 100644 index 0000000000..0a93851aed --- /dev/null +++ b/tests/extra/bats/helpers.bash @@ -0,0 +1,119 @@ +# Shared helpers for the bats extra-tests suite. + +# make_tmp_repo: create and cd into a fresh git-annex repo under $BATS_TEST_TMPDIR. +# Sets $REPO to the created path. +make_tmp_repo() { + REPO="$(mktemp -d "${BATS_TEST_TMPDIR:-/tmp}/annex.XXXXXX")" + ( + cd "$REPO" + git init -q + git config user.email "test@github.land" + git config user.name "GitHub Almighty" + git annex init -q + ) +} + +# require_cmd [reason]: skip the test if is not on PATH. +require_cmd() { + local cmd=$1 + local reason=${2:-"$cmd is not installed"} + command -v "$cmd" >/dev/null 2>&1 || skip "$reason" +} + +# require_linux: skip if not running on Linux. +require_linux() { + [[ "$(uname -s)" == "Linux" ]] || skip "test is Linux-only" +} + +# git_annex_version: prints just the numeric version, e.g. "10.20260421". +# Strips the "-g" build suffix. Empty output on error. +git_annex_version() { + command -v git-annex >/dev/null 2>&1 || return 0 + git annex version 2>/dev/null \ + | awk -F': ' '/^git-annex version:/ { split($2, a, "-"); print a[1] }' +} + +# git_annex_version_below : exit 0 (true) if the installed +# git-annex version is strictly older than , else exit 1. +# Uses `sort -V` for version-aware comparison. +git_annex_version_below() { + local threshold=$1 + local v + v=$(git_annex_version) + [[ -z "$v" ]] && return 1 + # If sorted-V-ascending puts $v first and $threshold second AND they + # differ, then $v < $threshold. + local first + first=$(printf '%s\n%s\n' "$v" "$threshold" | sort -V | head -n1) + [[ "$first" == "$v" && "$v" != "$threshold" ]] +} + +# git_annex_releases_since : count git-annex release tags +# in the current repository that are strictly newer than . +# Prints the count on stdout; empty on error / no tags found. +git_annex_releases_since() { + local threshold=$1 + command -v git >/dev/null 2>&1 || return 0 + local tags + tags=$(git tag --list '10.*' 2>/dev/null) || return 0 + [[ -z "$tags" ]] && return 0 + # Newer-than test with sort -V: keep tags strictly > threshold. + local newer + newer=$(printf '%s\n' "$tags" \ + | awk -v t="$threshold" '$0 > t') # lex compare is fine for 10.YYYYMMDD + # Refine with sort -V to be safe for oddly-shaped tags. + newer=$(printf '%s\n' "$newer" \ + | while read -r tag; do + [[ -z "$tag" ]] && continue + first=$(printf '%s\n%s\n' "$tag" "$threshold" | sort -V | head -n1) + [[ "$first" == "$threshold" && "$tag" != "$threshold" ]] && echo "$tag" + done) + printf '%s\n' "$newer" | grep -c . +} + +# print_versions: emit the same "extra-tests" header the pytest +# conftest prints, so bats runs are self-describing too. Written to +# BATS's own fd (3) so it's visible without --show-output-of-passing-tests. +print_versions() { + local tools_line="" + local sep="" + for entry in "git|git --version" "bats|bats --version" \ + "yt-dlp|yt-dlp --version" "youtube-dl|youtube-dl --version" \ + "strace|strace --version" + do + local name=${entry%%|*} + local cmd=${entry#*|} + local v + if command -v "${cmd%% *}" >/dev/null 2>&1; then + v=$($cmd 2>/dev/null | head -n1) + else + v="(missing)" + fi + tools_line+="${sep}${name}=${v}" + sep=", " + done + + { + echo "# extra-tests tools: $tools_line" + echo "# platform: $(uname -sr) $(uname -m)" + echo "# git-annex:" + git annex version 2>/dev/null | awk ' + /^git-annex version:|^build flags:|^dependency versions:|^operating system:|^supported repository versions:/ { + print "# " $0 + } + ' + local installed + installed=$(git_annex_version) + if [[ -z "$installed" ]]; then + echo "# git-annex releases newer than installed: (git-annex not installed)" + else + local n + n=$(git_annex_releases_since "$installed") + if [[ -n "$n" ]]; then + echo "# git-annex releases in this repo newer than installed (${installed}): ${n}" + else + echo "# git-annex releases in this repo newer than installed (${installed}): (no tags found)" + fi + fi + } >&3 +} diff --git a/tests/extra/bats/setup_suite.bash b/tests/extra/bats/setup_suite.bash new file mode 100644 index 0000000000..0795905450 --- /dev/null +++ b/tests/extra/bats/setup_suite.bash @@ -0,0 +1,8 @@ +# Suite-wide setup for tests/extra/bats/. Bats-core loads this file +# automatically when running a directory and calls setup_suite once +# before any test. See . + +setup_suite() { + load helpers + print_versions +} diff --git a/tests/extra/bats/url_backend.bats b/tests/extra/bats/url_backend.bats new file mode 100644 index 0000000000..e3b1d6b7f0 --- /dev/null +++ b/tests/extra/bats/url_backend.bats @@ -0,0 +1,58 @@ +#!/usr/bin/env bats +# +# Regression: older git-annex failed on URL-backend keys whose encoded name +# contained characters like `&c`, `%%`, `,63v`. See +# tests/extra/pytest/test_url_backend.py for the equivalent pytest version. + +load helpers + +REPRO_URL='https://datasets.datalad.org/repronim/ReproTube/DataLad/.git/' +TARGET='videos/2021/07/2021-07-11_Demo-Fully-recomputing-a-real-scientific-paper-DIY/video.mkv' + +# git-annex releases strictly older than this have the URL-encoded-key +# retrieval bug (upstream commit 8fd9b67ed8, first shipped in +# 10.20260420). bats has no direct xfail; we `skip` on older versions, +# so a regression on a fixed version fails loudly and older versions +# don't red the run. Keep in sync with URL_BACKEND_FIX_VERSION in +# tests/extra/pytest/test_url_backend.py. +URL_BACKEND_FIX_VERSION='10.20260420' + +setup_file() { + REPO_PARENT="$(mktemp -d "${BATS_FILE_TMPDIR:-/tmp}/ReproTube.XXXXXX")" + REPO="$REPO_PARENT/DataLad" + export REPO REPO_PARENT + # --no-single-branch so we also fetch the git-annex branch, which is + # where URL-backend metadata lives. + git clone --depth=1 --no-single-branch "$REPRO_URL" "$REPO" + ( + cd "$REPO" + git config user.email "test@github.land" + git config user.name "GitHub Almighty" + git annex init -q + ) +} + +teardown_file() { + if [[ -n "${REPO_PARENT:-}" && -d "$REPO_PARENT" ]]; then + # git-annex objects are read-only; make them writable before rm. + chmod -R u+w "$REPO_PARENT" 2>/dev/null || true + rm -rf "$REPO_PARENT" + fi + return 0 +} + +@test "whereis decodes URL-backend key back to youtube.com/watch?v=" { + cd "$REPO" + run git annex whereis "$TARGET" + [ "$status" -eq 0 ] + echo "$output" | grep -q 'youtube.com/watch?v=' +} + +@test "get retrieves URL-backend file" { + if git_annex_version_below "$URL_BACKEND_FIX_VERSION"; then + skip "URL-encoded-key retrieval bug in git-annex < $URL_BACKEND_FIX_VERSION (installed: $(git_annex_version))" + fi + cd "$REPO" + timeout 600 git annex get "$TARGET" + [ -s "$TARGET" ] +} diff --git a/tests/extra/pytest/test_dynlibs.py b/tests/extra/pytest/test_dynlibs.py new file mode 100644 index 0000000000..2081df18ac --- /dev/null +++ b/tests/extra/pytest/test_dynlibs.py @@ -0,0 +1,54 @@ +""" +Guard against regressions in git-annex's dynamic-library lookup behaviour. + +Older git-annex builds probed hundreds of directories for libpcre before +finding it, causing measurable startup slowdowns on some filesystems. +The check is a strace over `git-annex version` / `git-annex init`, +counting ENOENT lookups whose path matches `libpcre.*so`, and asserting +the count stays below a known-reasonable ceiling. + +Linux-only: strace has no cross-platform equivalent that is trivial to +substitute here. +""" + +from __future__ import annotations + +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.skipif( + not sys.platform.startswith("linux") or shutil.which("strace") is None, + reason="strace is Linux-only", +) + + +def _count_enoent(subcommand: str, pattern: str, cwd: Path) -> int: + """Return the number of ENOENT lines matching `pattern` under strace.""" + result = subprocess.run( + ["strace", "-f", "git-annex", subcommand], + cwd=cwd, + capture_output=True, + text=True, + ) + regex = re.compile(rf"{pattern}.*ENOENT") + matches = [line for line in result.stderr.splitlines() if regex.search(line)] + for m in matches: + print(m, file=sys.stderr) + return len(matches) + + +def test_libpcre_lookups_on_version(tmp_path: Path) -> None: + subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True) + n = _count_enoent("version", r"libpcre.*so", tmp_path) + assert n < 7, f"too many libpcre ENOENT lookups on `git-annex version`: {n}" + + +def test_libpcre_lookups_on_init(tmp_path: Path) -> None: + subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True) + n = _count_enoent("init", r"libpcre.*so", tmp_path) + assert n < 260, f"too many libpcre ENOENT lookups on `git-annex init`: {n}" diff --git a/tests/extra/pytest/test_url_backend.py b/tests/extra/pytest/test_url_backend.py new file mode 100644 index 0000000000..ecdc48e52e --- /dev/null +++ b/tests/extra/pytest/test_url_backend.py @@ -0,0 +1,107 @@ +""" +Regression: older git-annex failed on URL-backend keys whose encoded name +contained characters like `&c`, `%%`, `,63v` (from URL-encoded scheme, +`://`, `?v=`, etc.). The reproducer is a real DataLad dataset that +stores YouTube videos with `yt:` keys. + +Two levels of check on a URL-backend key that decodes to a `yt:` URL: + 1. `git annex whereis` — parses the key and lists its recorded URLs. + This code path was NOT affected by the bug, + and passes on all git-annex versions. + 2. `git annex get` — retrieves the ~18 MB video file, falling + back through the recorded remotes. This + was the affected code path; xfail on + git-annex versions below the fix. + +The DataLad dataset's `origin` remote serves the annex content over +HTTPS, so the retrieval does not require yt-dlp or YouTube access. +""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from conftest import ( + URL_BACKEND_FIX_VERSION, + git_annex_version, + git_annex_version_below, +) + +# URL_BACKEND_FIX_VERSION is the git-annex release that first shipped +# the fix (upstream commit 8fd9b67ed8 "factor out extendUrlWithPath …", +# 2026-02-16). Older versions xfail so we do not block CI on a known +# regression while still guaranteeing that once a build is on a fixed +# version, the test acts as a permanent regression guard. + +_xfail_broken_url_backend = pytest.mark.xfail( + condition=git_annex_version_below(URL_BACKEND_FIX_VERSION), + reason=( + f"URL-encoded-key retrieval bug present in git-annex " + f"< {URL_BACKEND_FIX_VERSION} " + f"(installed: {git_annex_version() or 'unknown'})" + ), + strict=False, +) + +REPRO_URL = "https://datasets.datalad.org/repronim/ReproTube/DataLad/.git/" +TARGET = ( + "videos/2021/07/" + "2021-07-11_Demo-Fully-recomputing-a-real-scientific-paper-DIY/" + "video.mkv" +) + + +@pytest.fixture(scope="module") +def cloned_repo(tmp_path_factory: pytest.TempPathFactory) -> Path: + workdir = tmp_path_factory.mktemp("ReproTube") + repo = workdir / "DataLad" + # --no-single-branch so we also fetch the git-annex branch, which is + # where URL-backend metadata lives. + subprocess.run( + ["git", "clone", "--depth=1", "--no-single-branch", REPRO_URL, str(repo)], + check=True, + ) + subprocess.run( + ["git", "config", "user.email", "test@github.land"], + cwd=repo, check=True, + ) + subprocess.run( + ["git", "config", "user.name", "GitHub Almighty"], + cwd=repo, check=True, + ) + subprocess.run(["git", "annex", "init", "-q"], cwd=repo, check=True) + return repo + + +def test_whereis_parses_url_backend_key(cloned_repo: Path) -> None: + """`git annex whereis` on a URL-backend key must list the decoded URL.""" + result = subprocess.run( + ["git", "annex", "whereis", TARGET], + cwd=cloned_repo, + capture_output=True, + text=True, + check=True, + ) + out = result.stdout + # The `,63v,61` chars in the key are the URL-encoded `?v=`; + # git-annex must decode them back to the original YouTube URL. + assert "youtube.com/watch?v=" in out, ( + f"expected decoded youtube URL in `whereis` output; got:\n{out}" + ) + + +@_xfail_broken_url_backend +def test_get_url_backend_key(cloned_repo: Path) -> None: + """Full reproducer: retrieve the URL-backend file.""" + subprocess.run( + ["git", "annex", "get", TARGET], + cwd=cloned_repo, + check=True, + timeout=600, + ) + target = cloned_repo / TARGET + assert target.exists(), f"{TARGET} was not retrieved" + assert target.stat().st_size > 0, f"{TARGET} is empty after get" From 72f17ba1ae21943898b72c218b7eee6d010d231b Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Sun, 2 Aug 2026 10:11:50 -0400 Subject: [PATCH 2/9] Drop bats prototype; pytest is the extra-tests suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five independent senior-engineer reviews of the prior side-by-side bats + pytest prototype unanimously recommended pytest. The short version of their reasoning, preserved in tests/extra/README.md so this question doesn't get re-litigated: - Cross-platform install cost: bats needs three distinct install recipes (Ubuntu apt / macOS brew / Windows git-clone bootstrap) and runs under Git Bash on Windows where `timeout`, `chmod -R u+w`, and `sort -V` behave subtly differently. pytest is one `pip install` on all four runners, and Python is already required by test-datalad. - No real xfail primitive in bats: `skip` cannot distinguish "known-broken on this version" from "unexpectedly passed" — a materially weaker regression signal for the URL-backend `get` test. - Fixtures and shared helpers: `conftest.py` gives cached `git annex version` parsing, session-scoped module clones, and parametrization out of the box; the bats port re-implemented each by hand. Removed: - tests/extra/bats/{dynlibs.bats,url_backend.bats,helpers.bash, setup_suite.bash}. - The "Install bats" (apt / brew / git-clone) template branches and the "Run bats suite" step in .github/workflows/template/build-{{ostype}}.yaml.j2. The Ubuntu step is now "Install strace" (previously bundled strace + bats). - Regenerated the four concrete workflow YAMLs via .github/workflows/template/mkworkflows.py; YAML-linted clean. tests/extra/README.md rewritten to describe the pytest-only setup and to explain why bats was evaluated and rejected. `reuse lint` still clean (45/45). Local pytest run: 4 passed. Concrete bugs the reviewers also flagged in the pytest side (strace exit-code check, `from conftest import ...` fragility, xfail evaluated-at-import without a git-annex-present guard, Windows read-only rmtree cleanup, dead git_annex_repo fixture) are left for a follow-up commit to keep the "drop bats" change reviewable in isolation. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- .github/workflows/build-macos-arm64.yaml | 13 +- .github/workflows/build-macos.yaml | 13 +- .github/workflows/build-ubuntu.yaml | 15 +-- .github/workflows/build-windows.yaml | 15 +-- .../template/build-{{ostype}}.yaml.j2 | 23 +--- tests/conftest.py | 1 - tests/extra/README.md | 58 +++++---- tests/extra/bats/dynlibs.bats | 39 ------ tests/extra/bats/helpers.bash | 119 ------------------ tests/extra/bats/setup_suite.bash | 8 -- tests/extra/bats/url_backend.bats | 58 --------- 11 files changed, 59 insertions(+), 303 deletions(-) delete mode 100644 tests/extra/bats/dynlibs.bats delete mode 100644 tests/extra/bats/helpers.bash delete mode 100644 tests/extra/bats/setup_suite.bash delete mode 100644 tests/extra/bats/url_backend.bats diff --git a/.github/workflows/build-macos-arm64.yaml b/.github/workflows/build-macos-arm64.yaml index 60d35fdcf9..d660ccff9a 100644 --- a/.github/workflows/build-macos-arm64.yaml +++ b/.github/workflows/build-macos-arm64.yaml @@ -341,10 +341,10 @@ jobs: See for more information. test-extra: - # Runs the extra-tests suite in tests/extra/ (both pytest and bats - # variants). Each test declares its own skip conditions, so the - # default is: run on every platform, skip only where an individual - # test's dependencies aren't available (e.g. dynlibs needs strace). + # Runs the pytest extra-tests suite in tests/extra/pytest/. Each + # test declares its own skip conditions, so the default is: run on + # every platform, skip only where an individual test's dependencies + # aren't available (e.g. dynlibs needs strace). runs-on: macos-15 needs: build-package steps: @@ -374,8 +374,6 @@ jobs: hdiutil detach /Volumes/git-annex/ echo /Applications/git-annex.app/Contents/MacOS >> "$GITHUB_PATH" - - name: Install bats - run: brew install bats-core - name: Set up Python uses: actions/setup-python@v6 @@ -393,9 +391,6 @@ jobs: - name: Run pytest suite run: python -m pytest -v tests/extra/pytest/ - - name: Run bats suite - run: bats tests/extra/bats/ - - name: Set final PR status if: always() && github.event.inputs.pr != '' run: | diff --git a/.github/workflows/build-macos.yaml b/.github/workflows/build-macos.yaml index 49823ab0d6..f3ccd098ec 100644 --- a/.github/workflows/build-macos.yaml +++ b/.github/workflows/build-macos.yaml @@ -340,10 +340,10 @@ jobs: See for more information. test-extra: - # Runs the extra-tests suite in tests/extra/ (both pytest and bats - # variants). Each test declares its own skip conditions, so the - # default is: run on every platform, skip only where an individual - # test's dependencies aren't available (e.g. dynlibs needs strace). + # Runs the pytest extra-tests suite in tests/extra/pytest/. Each + # test declares its own skip conditions, so the default is: run on + # every platform, skip only where an individual test's dependencies + # aren't available (e.g. dynlibs needs strace). runs-on: macos-15-intel needs: build-package steps: @@ -373,8 +373,6 @@ jobs: hdiutil detach /Volumes/git-annex/ echo /Applications/git-annex.app/Contents/MacOS >> "$GITHUB_PATH" - - name: Install bats - run: brew install bats-core - name: Set up Python uses: actions/setup-python@v6 @@ -392,9 +390,6 @@ jobs: - name: Run pytest suite run: python -m pytest -v tests/extra/pytest/ - - name: Run bats suite - run: bats tests/extra/bats/ - - name: Set final PR status if: always() && github.event.inputs.pr != '' run: | diff --git a/.github/workflows/build-ubuntu.yaml b/.github/workflows/build-ubuntu.yaml index 3cadc48d96..74f439116a 100644 --- a/.github/workflows/build-ubuntu.yaml +++ b/.github/workflows/build-ubuntu.yaml @@ -378,10 +378,10 @@ jobs: See for more information. test-extra: - # Runs the extra-tests suite in tests/extra/ (both pytest and bats - # variants). Each test declares its own skip conditions, so the - # default is: run on every platform, skip only where an individual - # test's dependencies aren't available (e.g. dynlibs needs strace). + # Runs the pytest extra-tests suite in tests/extra/pytest/. Each + # test declares its own skip conditions, so the default is: run on + # every platform, skip only where an individual test's dependencies + # aren't available (e.g. dynlibs needs strace). runs-on: ubuntu-24.04 needs: build-package steps: @@ -408,8 +408,8 @@ jobs: run: | sudo dpkg -i git-annex*.deb - - name: Install strace and bats - run: sudo apt-get update -qq && sudo apt-get install -y strace bats + - name: Install strace + run: sudo apt-get update -qq && sudo apt-get install -y strace - name: Set up Python uses: actions/setup-python@v6 @@ -427,9 +427,6 @@ jobs: - name: Run pytest suite run: python -m pytest -v tests/extra/pytest/ - - name: Run bats suite - run: bats tests/extra/bats/ - - name: Set final PR status if: always() && github.event.inputs.pr != '' run: | diff --git a/.github/workflows/build-windows.yaml b/.github/workflows/build-windows.yaml index a80fe0b8ea..5f986d6ef8 100644 --- a/.github/workflows/build-windows.yaml +++ b/.github/workflows/build-windows.yaml @@ -326,10 +326,10 @@ jobs: See for more information. test-extra: - # Runs the extra-tests suite in tests/extra/ (both pytest and bats - # variants). Each test declares its own skip conditions, so the - # default is: run on every platform, skip only where an individual - # test's dependencies aren't available (e.g. dynlibs needs strace). + # Runs the pytest extra-tests suite in tests/extra/pytest/. Each + # test declares its own skip conditions, so the default is: run on + # every platform, skip only where an individual test's dependencies + # aren't available (e.g. dynlibs needs strace). runs-on: windows-2025 needs: build-package steps: @@ -357,10 +357,6 @@ jobs: run: | ./git-annex-installer_*.exe /S - - name: Install bats - run: | - git clone --depth=1 https://github.com/bats-core/bats-core.git "$RUNNER_TEMP/bats-core" - echo "$RUNNER_TEMP/bats-core/bin" >> "$GITHUB_PATH" - name: Set up Python uses: actions/setup-python@v6 @@ -378,9 +374,6 @@ jobs: - name: Run pytest suite run: python -m pytest -v tests/extra/pytest/ - - name: Run bats suite - run: bats tests/extra/bats/ - - name: Set final PR status if: always() && github.event.inputs.pr != '' run: | diff --git a/.github/workflows/template/build-{{ostype}}.yaml.j2 b/.github/workflows/template/build-{{ostype}}.yaml.j2 index a0c3417103..161a1b7c7f 100644 --- a/.github/workflows/template/build-{{ostype}}.yaml.j2 +++ b/.github/workflows/template/build-{{ostype}}.yaml.j2 @@ -571,10 +571,10 @@ jobs: See for more information. test-extra: - # Runs the extra-tests suite in tests/extra/ (both pytest and bats - # variants). Each test declares its own skip conditions, so the - # default is: run on every platform, skip only where an individual - # test's dependencies aren't available (e.g. dynlibs needs strace). + # Runs the pytest extra-tests suite in tests/extra/pytest/. Each + # test declares its own skip conditions, so the default is: run on + # every platform, skip only where an individual test's dependencies + # aren't available (e.g. dynlibs needs strace). runs-on: {{runs_on}} needs: build-package steps: @@ -607,16 +607,8 @@ jobs: {% endfor %} {% if ostype == "ubuntu" %} - - name: Install strace and bats - run: sudo apt-get update -qq && sudo apt-get install -y strace bats - {% elif ostype.startswith("macos") %} - - name: Install bats - run: brew install bats-core - {% elif ostype == "windows" %} - - name: Install bats - run: | - git clone --depth=1 https://github.com/bats-core/bats-core.git "$RUNNER_TEMP/bats-core" - echo "$RUNNER_TEMP/bats-core/bin" >> "$GITHUB_PATH" + - name: Install strace + run: sudo apt-get update -qq && sudo apt-get install -y strace {% endif %} - name: Set up Python @@ -635,9 +627,6 @@ jobs: - name: Run pytest suite run: python -m pytest -v tests/extra/pytest/ - - name: Run bats suite - run: bats tests/extra/bats/ - - name: Set final PR status if: always() && github.event.inputs.pr != '' run: | diff --git a/tests/conftest.py b/tests/conftest.py index d723b041da..4d77efebfd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,7 +159,6 @@ def pytest_report_header(config: pytest.Config) -> list[str]: """Version + tool info at the top of pytest's session banner.""" tools = [ ("git", ["git", "--version"]), - ("bats", ["bats", "--version"]), ("yt-dlp", ["yt-dlp", "--version"]), ("youtube-dl", ["youtube-dl", "--version"]), ("strace", ["strace", "--version"]), diff --git a/tests/extra/README.md b/tests/extra/README.md index 0163a913c1..b10e2b7e69 100644 --- a/tests/extra/README.md +++ b/tests/extra/README.md @@ -4,36 +4,48 @@ Tests that CI runs on top of `git annex test` and the DataLad test battery. Each test targets a specific real-world scenario, often a regression that was seen in the wild and would slip past both upstream test suites. -Two parallel implementations of the same suite are kept side-by-side so we -can compare style and eventually pick one to promote (upstream-friendly or -otherwise): - -- `pytest/` — Python + pytest. Fixtures for temp directories and repo - cloning; skip markers are `@pytest.mark.skipif(...)`. -- `bats/` — [Bats](https://bats-core.readthedocs.io/) shell tests. Skip - logic uses the `skip` built-in inside `setup()`. - -CI runs **both**. Each test declares its own skip conditions (missing -`strace`, missing `yt-dlp`, unsupported platform) so the default is: run -everywhere, skip only where the required tool isn't available. +Written for **pytest**. Each test declares its own skip conditions +(missing `strace`, unsupported platform, git-annex not on PATH) so the +default is: run everywhere, skip only where the required tool isn't +available. The URL-backend `get` test is `xfail(strict=False)` below +the known-fix version so old git-annex builds do not red the run but a +regression on a fixed build fails loudly. + +## Why pytest and not bats? + +A parallel [Bats](https://bats-core.readthedocs.io/) prototype was +evaluated and dropped. The reasons, briefly: + +- **Cross-platform install cost.** Bats needs three distinct install + recipes (Ubuntu `apt`, macOS `brew`, Windows git-clone bootstrap) + and runs under Git Bash on Windows where `timeout`, `chmod -R u+w` + and `sort -V` behave subtly differently. Pytest is one + `pip install pytest` on all four runners, and Python is already + needed by the `test-datalad` job. +- **No real `xfail` primitive.** Bats only has `skip`, which cannot + distinguish "known-broken on this version, expected to fail" from + "unexpectedly passed, tell me". pytest's + `@pytest.mark.xfail(strict=False)` gives the correct + regression-guard semantics on the URL-backend `get` test. +- **Fixtures and shared helpers.** `conftest.py` gives us cached + `git annex version` parsing, a shared version-reporting hook, + `tmp_path_factory` module-scoped clones, and clean parametrization + — all of which the bats port was re-implementing by hand in + progressively-hairier shell. ## Tests -| Test | Purpose | Platforms | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------- | -| `dynlibs` | Regression guard on the number of failed dynamic-library lookups (`strace -e ENOENT` on `libpcre.*so`) during `git-annex version`/`init`. | Linux | -| `url_backend` | Regression guard for parsing "odd" URL-backend keys (URL-encoded characters like `&c`, `%%`, `,63v` etc.) on a real DataLad dataset. | All | +| Test | Purpose | Platforms | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `dynlibs` | Regression guard on the number of failed dynamic-library lookups (`strace -e ENOENT` on `libpcre.*so`) during `git-annex version` / `init`. | Linux | +| `url_backend` | Regression guard for parsing "odd" URL-backend keys (URL-encoded characters like `&c`, `%%`, `,63v` etc.) on a real DataLad dataset. | All | ## Running locally ```bash -# pytest suite python -m pytest -v tests/extra/pytest/ - -# bats suite (needs bats-core installed) -bats tests/extra/bats/ ``` -Both suites assume `git-annex` is on `PATH`. The `url_backend` test -clones a small (~18 MB) real DataLad dataset from `datasets.datalad.org` -so needs network access. +Assumes `git-annex` is on `PATH`. The `url_backend` test clones a +small (~18 MB) real DataLad dataset from `datasets.datalad.org`, so +needs network access. diff --git a/tests/extra/bats/dynlibs.bats b/tests/extra/bats/dynlibs.bats deleted file mode 100644 index 351b036047..0000000000 --- a/tests/extra/bats/dynlibs.bats +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bats -# -# Guard against regressions in git-annex's dynamic-library lookup behaviour. -# See tests/extra/pytest/test_dynlibs.py for the equivalent pytest version. - -load helpers - -setup() { - require_linux - require_cmd strace - TESTREPO="$(mktemp -d "${BATS_TEST_TMPDIR:-/tmp}/dynlibs.XXXXXX")" - ( cd "$TESTREPO" && git init -q ) -} - -teardown() { - [[ -n "${TESTREPO:-}" && -d "$TESTREPO" ]] && rm -rf "$TESTREPO" - return 0 -} - -nfailed() { - local subcommand=$1 - local pattern=$2 - strace -f git-annex "$subcommand" 2>&1 \ - | awk "/${pattern}.*ENOENT/{print}" \ - | tee /dev/fd/2 \ - | wc -l -} - -@test "libpcre ENOENT lookups on 'git-annex version' < 7" { - cd "$TESTREPO" - n=$(nfailed version "libpcre.*so") - [ "$n" -lt 7 ] -} - -@test "libpcre ENOENT lookups on 'git-annex init' < 260" { - cd "$TESTREPO" - n=$(nfailed init "libpcre.*so") - [ "$n" -lt 260 ] -} diff --git a/tests/extra/bats/helpers.bash b/tests/extra/bats/helpers.bash deleted file mode 100644 index 0a93851aed..0000000000 --- a/tests/extra/bats/helpers.bash +++ /dev/null @@ -1,119 +0,0 @@ -# Shared helpers for the bats extra-tests suite. - -# make_tmp_repo: create and cd into a fresh git-annex repo under $BATS_TEST_TMPDIR. -# Sets $REPO to the created path. -make_tmp_repo() { - REPO="$(mktemp -d "${BATS_TEST_TMPDIR:-/tmp}/annex.XXXXXX")" - ( - cd "$REPO" - git init -q - git config user.email "test@github.land" - git config user.name "GitHub Almighty" - git annex init -q - ) -} - -# require_cmd [reason]: skip the test if is not on PATH. -require_cmd() { - local cmd=$1 - local reason=${2:-"$cmd is not installed"} - command -v "$cmd" >/dev/null 2>&1 || skip "$reason" -} - -# require_linux: skip if not running on Linux. -require_linux() { - [[ "$(uname -s)" == "Linux" ]] || skip "test is Linux-only" -} - -# git_annex_version: prints just the numeric version, e.g. "10.20260421". -# Strips the "-g" build suffix. Empty output on error. -git_annex_version() { - command -v git-annex >/dev/null 2>&1 || return 0 - git annex version 2>/dev/null \ - | awk -F': ' '/^git-annex version:/ { split($2, a, "-"); print a[1] }' -} - -# git_annex_version_below : exit 0 (true) if the installed -# git-annex version is strictly older than , else exit 1. -# Uses `sort -V` for version-aware comparison. -git_annex_version_below() { - local threshold=$1 - local v - v=$(git_annex_version) - [[ -z "$v" ]] && return 1 - # If sorted-V-ascending puts $v first and $threshold second AND they - # differ, then $v < $threshold. - local first - first=$(printf '%s\n%s\n' "$v" "$threshold" | sort -V | head -n1) - [[ "$first" == "$v" && "$v" != "$threshold" ]] -} - -# git_annex_releases_since : count git-annex release tags -# in the current repository that are strictly newer than . -# Prints the count on stdout; empty on error / no tags found. -git_annex_releases_since() { - local threshold=$1 - command -v git >/dev/null 2>&1 || return 0 - local tags - tags=$(git tag --list '10.*' 2>/dev/null) || return 0 - [[ -z "$tags" ]] && return 0 - # Newer-than test with sort -V: keep tags strictly > threshold. - local newer - newer=$(printf '%s\n' "$tags" \ - | awk -v t="$threshold" '$0 > t') # lex compare is fine for 10.YYYYMMDD - # Refine with sort -V to be safe for oddly-shaped tags. - newer=$(printf '%s\n' "$newer" \ - | while read -r tag; do - [[ -z "$tag" ]] && continue - first=$(printf '%s\n%s\n' "$tag" "$threshold" | sort -V | head -n1) - [[ "$first" == "$threshold" && "$tag" != "$threshold" ]] && echo "$tag" - done) - printf '%s\n' "$newer" | grep -c . -} - -# print_versions: emit the same "extra-tests" header the pytest -# conftest prints, so bats runs are self-describing too. Written to -# BATS's own fd (3) so it's visible without --show-output-of-passing-tests. -print_versions() { - local tools_line="" - local sep="" - for entry in "git|git --version" "bats|bats --version" \ - "yt-dlp|yt-dlp --version" "youtube-dl|youtube-dl --version" \ - "strace|strace --version" - do - local name=${entry%%|*} - local cmd=${entry#*|} - local v - if command -v "${cmd%% *}" >/dev/null 2>&1; then - v=$($cmd 2>/dev/null | head -n1) - else - v="(missing)" - fi - tools_line+="${sep}${name}=${v}" - sep=", " - done - - { - echo "# extra-tests tools: $tools_line" - echo "# platform: $(uname -sr) $(uname -m)" - echo "# git-annex:" - git annex version 2>/dev/null | awk ' - /^git-annex version:|^build flags:|^dependency versions:|^operating system:|^supported repository versions:/ { - print "# " $0 - } - ' - local installed - installed=$(git_annex_version) - if [[ -z "$installed" ]]; then - echo "# git-annex releases newer than installed: (git-annex not installed)" - else - local n - n=$(git_annex_releases_since "$installed") - if [[ -n "$n" ]]; then - echo "# git-annex releases in this repo newer than installed (${installed}): ${n}" - else - echo "# git-annex releases in this repo newer than installed (${installed}): (no tags found)" - fi - fi - } >&3 -} diff --git a/tests/extra/bats/setup_suite.bash b/tests/extra/bats/setup_suite.bash deleted file mode 100644 index 0795905450..0000000000 --- a/tests/extra/bats/setup_suite.bash +++ /dev/null @@ -1,8 +0,0 @@ -# Suite-wide setup for tests/extra/bats/. Bats-core loads this file -# automatically when running a directory and calls setup_suite once -# before any test. See . - -setup_suite() { - load helpers - print_versions -} diff --git a/tests/extra/bats/url_backend.bats b/tests/extra/bats/url_backend.bats deleted file mode 100644 index e3b1d6b7f0..0000000000 --- a/tests/extra/bats/url_backend.bats +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bats -# -# Regression: older git-annex failed on URL-backend keys whose encoded name -# contained characters like `&c`, `%%`, `,63v`. See -# tests/extra/pytest/test_url_backend.py for the equivalent pytest version. - -load helpers - -REPRO_URL='https://datasets.datalad.org/repronim/ReproTube/DataLad/.git/' -TARGET='videos/2021/07/2021-07-11_Demo-Fully-recomputing-a-real-scientific-paper-DIY/video.mkv' - -# git-annex releases strictly older than this have the URL-encoded-key -# retrieval bug (upstream commit 8fd9b67ed8, first shipped in -# 10.20260420). bats has no direct xfail; we `skip` on older versions, -# so a regression on a fixed version fails loudly and older versions -# don't red the run. Keep in sync with URL_BACKEND_FIX_VERSION in -# tests/extra/pytest/test_url_backend.py. -URL_BACKEND_FIX_VERSION='10.20260420' - -setup_file() { - REPO_PARENT="$(mktemp -d "${BATS_FILE_TMPDIR:-/tmp}/ReproTube.XXXXXX")" - REPO="$REPO_PARENT/DataLad" - export REPO REPO_PARENT - # --no-single-branch so we also fetch the git-annex branch, which is - # where URL-backend metadata lives. - git clone --depth=1 --no-single-branch "$REPRO_URL" "$REPO" - ( - cd "$REPO" - git config user.email "test@github.land" - git config user.name "GitHub Almighty" - git annex init -q - ) -} - -teardown_file() { - if [[ -n "${REPO_PARENT:-}" && -d "$REPO_PARENT" ]]; then - # git-annex objects are read-only; make them writable before rm. - chmod -R u+w "$REPO_PARENT" 2>/dev/null || true - rm -rf "$REPO_PARENT" - fi - return 0 -} - -@test "whereis decodes URL-backend key back to youtube.com/watch?v=" { - cd "$REPO" - run git annex whereis "$TARGET" - [ "$status" -eq 0 ] - echo "$output" | grep -q 'youtube.com/watch?v=' -} - -@test "get retrieves URL-backend file" { - if git_annex_version_below "$URL_BACKEND_FIX_VERSION"; then - skip "URL-encoded-key retrieval bug in git-annex < $URL_BACKEND_FIX_VERSION (installed: $(git_annex_version))" - fi - cd "$REPO" - timeout 600 git annex get "$TARGET" - [ -s "$TARGET" ] -} From e5d91fa5fd97fbccd2df3b79e94b7079ce60a171 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 09:07:44 -0400 Subject: [PATCH 3/9] Delete unused git_annex_repo fixture from tests/conftest.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture was seeded at the start of the extra-tests scaffolding work but no test ever consumed it — test_dynlibs and test_url_backend each build their own repo inline. Reviewer #1 flagged it as dead code; removing it (along with the now-unused Path import) keeps conftest.py to just the version-helper API and the report hook. No behaviour change; 4/4 pytest tests still pass. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- tests/conftest.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4d77efebfd..58aa600dc1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,30 +15,10 @@ import shutil import subprocess from functools import lru_cache -from pathlib import Path import pytest -# --- shared fixtures ---------------------------------------------------------- - - -@pytest.fixture -def git_annex_repo(tmp_path: Path) -> Path: - """An empty, initialised git-annex repository in a fresh temp dir.""" - subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True) - subprocess.run( - ["git", "config", "user.email", "test@github.land"], - cwd=tmp_path, check=True, - ) - subprocess.run( - ["git", "config", "user.name", "GitHub Almighty"], - cwd=tmp_path, check=True, - ) - subprocess.run(["git", "annex", "init", "-q"], cwd=tmp_path, check=True) - return tmp_path - - # --- git-annex version helpers ----------------------------------------------- From 808d7a0b2f40541b8539d80f49ff0eee9a06ccaa Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 09:11:24 -0400 Subject: [PATCH 4/9] Extract version helpers to tests/_helpers.py Reviewer #1/#2/#3/#5 flagged `from conftest import ...` in test_url_backend.py as fragile: it relies on pytest's default --import-mode=prepend adding tests/ to sys.path via the conftest at that level. Under --import-mode=importlib the import fails, and running the test file directly from an unusual cwd is at the mercy of pytest's rootdir heuristics. Fix: move URL_BACKEND_FIX_VERSION and the git_annex_version / git_annex_version_below / git_annex_releases_since helpers into a plain module tests/_helpers.py. conftest.py now only holds the `pytest_report_header` hook (plus its two report-specific helpers `_first_line` and `_git_annex_summary`), and imports the shared bits from `_helpers`. To make `from _helpers import ...` work under any --import-mode, setup.cfg gets a `[tool:pytest]` section with `pythonpath = tests`. This is the pytest-native way to guarantee a directory ends up on sys.path regardless of import-mode / rootdir resolution. Verified both import modes: $ pytest tests # prepend (default) 4 passed $ pytest --import-mode=importlib tests/extra/... # importlib 2 passed Both invocations still print the "extra-tests tools" / git-annex version / releases-newer-than-installed header. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- setup.cfg | 6 ++ tests/_helpers.py | 100 +++++++++++++++++++++++++ tests/conftest.py | 95 +++-------------------- tests/extra/pytest/test_url_backend.py | 2 +- 4 files changed, 117 insertions(+), 86 deletions(-) create mode 100644 tests/_helpers.py diff --git a/setup.cfg b/setup.cfg index 686fdc64d8..4ad063e078 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,9 @@ +[tool:pytest] +# Put tests/ on sys.path so both the conftest and individual test +# modules can import shared helpers via `from _helpers import ...` +# under any --import-mode (prepend, importlib, ...). +pythonpath = tests + [flake8] doctests = True #max-doc-length = 100 diff --git a/tests/_helpers.py b/tests/_helpers.py new file mode 100644 index 0000000000..a38dbaf0fb --- /dev/null +++ b/tests/_helpers.py @@ -0,0 +1,100 @@ +""" +Shared helpers for the tests/ tree. + +Kept as a plain module (not a conftest.py) so both the pytest report +hook in tests/conftest.py and individual test modules can import via +`from _helpers import ...` without relying on pytest's conftest +import-name magic (which broke under `--import-mode=importlib` and +similar). + +pytest puts tests/ on sys.path via the conftest.py at that level, so +`from _helpers import ...` resolves for both callers. +""" + +from __future__ import annotations + +import shutil +import subprocess +from functools import lru_cache + + +# Fix version for the URL-encoded-key bug (upstream 8fd9b67ed8, +# 2026-02-16; first shipped in release 10.20260420). Single source of +# truth for both the xfail marker in test_url_backend.py and the +# version reporting in tests/conftest.py. +URL_BACKEND_FIX_VERSION = "10.20260420" + + +@lru_cache(maxsize=1) +def git_annex_version_output() -> str | None: + """Raw stdout of `git annex version`, cached for the session.""" + if shutil.which("git-annex") is None: + return None + try: + return subprocess.run( + ["git", "annex", "version"], + capture_output=True, text=True, timeout=15, check=True, + ).stdout + except (subprocess.SubprocessError, OSError): + return None + + +def git_annex_version() -> str | None: + """ + Bare version string reported by git-annex, e.g. "10.20260421" + (`-g` build suffix stripped). None if git-annex is not + installed / not runnable. + """ + out = git_annex_version_output() + if not out: + return None + for line in out.splitlines(): + if line.startswith("git-annex version:"): + return line.split(":", 1)[1].strip().split("-", 1)[0] + return None + + +def _version_key(s: str) -> tuple[int, ...]: + return tuple(int(p) for p in s.split(".") if p.isdigit()) + + +def git_annex_version_below(threshold: str) -> bool: + """ + True if the installed git-annex version is *strictly* below `threshold`. + Compares as tuples of ints on the "." separator ("10.20220615" < + "10.20260421"). False if git-annex is missing. + """ + v = git_annex_version() + if v is None: + return False + return _version_key(v) < _version_key(threshold) + + +@lru_cache(maxsize=32) +def git_annex_releases_since(threshold: str) -> int | None: + """ + Count git-annex release tags in the current repository that are + strictly newer than `threshold`. Returns None if not inside a git + repo or if there are no matching tags (e.g. a shallow checkout). + """ + if shutil.which("git") is None: + return None + try: + out = subprocess.run( + ["git", "tag", "--list", "10.*"], + capture_output=True, text=True, timeout=10, check=True, + ).stdout + except (subprocess.SubprocessError, OSError): + return None + key = _version_key(threshold) + n = 0 + for tag in out.splitlines(): + tag = tag.strip() + if not tag: + continue + try: + if _version_key(tag) > key: + n += 1 + except ValueError: + continue + return n if n or out.strip() else None diff --git a/tests/conftest.py b/tests/conftest.py index 58aa600dc1..9471434733 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,10 @@ pytest loads conftest.py files eagerly along the ancestor chain from each argument path down to rootdir; a conftest below the given path is loaded lazily during collection, which is too late for the header. + +Version / release helpers live in tests/_helpers.py so they are +importable both from here and from individual test modules without +relying on pytest's conftest-import magic. """ from __future__ import annotations @@ -14,92 +18,19 @@ import platform import shutil import subprocess -from functools import lru_cache import pytest - -# --- git-annex version helpers ----------------------------------------------- - - -@lru_cache(maxsize=1) -def _git_annex_version_output() -> str | None: - """Raw stdout of `git annex version`, cached for the session.""" - if shutil.which("git-annex") is None: - return None - try: - return subprocess.run( - ["git", "annex", "version"], - capture_output=True, text=True, timeout=15, check=True, - ).stdout - except (subprocess.SubprocessError, OSError): - return None - - -def git_annex_version() -> str | None: - """ - Bare version string reported by git-annex, e.g. "10.20260421" - (`-g` build suffix stripped). None if git-annex is not - installed / not runnable. - """ - out = _git_annex_version_output() - if not out: - return None - for line in out.splitlines(): - if line.startswith("git-annex version:"): - return line.split(":", 1)[1].strip().split("-", 1)[0] - return None - - -def _version_key(s: str) -> tuple[int, ...]: - return tuple(int(p) for p in s.split(".") if p.isdigit()) - - -def git_annex_version_below(threshold: str) -> bool: - """ - True if the installed git-annex version is *strictly* below `threshold`. - Compares as tuples of ints on the "." separator ("10.20220615" < - "10.20260421"). False if git-annex is missing. - """ - v = git_annex_version() - if v is None: - return False - return _version_key(v) < _version_key(threshold) - - -@lru_cache(maxsize=32) -def git_annex_releases_since(threshold: str) -> int | None: - """ - Count git-annex release tags in the current repository that are - strictly newer than `threshold`. Returns None if not inside a git - repo or if there are no matching tags (e.g. a shallow checkout). - """ - if shutil.which("git") is None: - return None - try: - out = subprocess.run( - ["git", "tag", "--list", "10.*"], - capture_output=True, text=True, timeout=10, check=True, - ).stdout - except (subprocess.SubprocessError, OSError): - return None - key = _version_key(threshold) - n = 0 - for tag in out.splitlines(): - tag = tag.strip() - if not tag: - continue - try: - if _version_key(tag) > key: - n += 1 - except ValueError: - continue - return n if n or out.strip() else None +from _helpers import ( + git_annex_releases_since, + git_annex_version, + git_annex_version_output, +) def _git_annex_summary() -> list[str]: """Critical fields from `git annex version` (no --json upstream).""" - out = _git_annex_version_output() + out = git_annex_version_output() if out is None: return ["git-annex: NOT INSTALLED"] wanted = ( @@ -129,12 +60,6 @@ def _first_line(cmd: list[str]) -> str | None: return out.splitlines()[0].strip() if out.strip() else None -# Fix version for the URL-encoded-key bug (upstream 8fd9b67ed8, -# 2026-02-16; first shipped in release 10.20260420). Kept here as a -# single source of truth so tests and the header agree. -URL_BACKEND_FIX_VERSION = "10.20260420" - - def pytest_report_header(config: pytest.Config) -> list[str]: """Version + tool info at the top of pytest's session banner.""" tools = [ diff --git a/tests/extra/pytest/test_url_backend.py b/tests/extra/pytest/test_url_backend.py index ecdc48e52e..e59a560dcc 100644 --- a/tests/extra/pytest/test_url_backend.py +++ b/tests/extra/pytest/test_url_backend.py @@ -24,7 +24,7 @@ import pytest -from conftest import ( +from _helpers import ( URL_BACKEND_FIX_VERSION, git_annex_version, git_annex_version_below, From 9fd70e83fcbf3e6b8866bd492c77e6ad2923a68d Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 09:12:10 -0400 Subject: [PATCH 5/9] Harden dynlibs _count_enoent against silent strace failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All five reviewers flagged the same issue: `subprocess.run(["strace", ...])` had no `check=`, no timeout, and no sanity check on stderr. On a runner where strace can't attach (`kernel.yama.ptrace_scope=1` or `=2`, seccomp filters, some container profiles) stderr is empty, the ENOENT count is 0, and the assertion `0 < 7` / `0 < 260` passes vacuously — meaning a regression that reintroduces hundreds of lookups would be missed. Fix: on top of the existing invocation, `_count_enoent` now - passes `timeout=120` so a hung strace fails the test instead of the whole CI job, - raises `RuntimeError` on non-zero exit, echoing the last 2KB of stderr for diagnostics, - raises when stderr contains no syscall / exit lines at all (`+++ exited`, ` ENOENT `, ` = `), which is the observable symptom of a blocked/refused strace. Local run still: 2 passed. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- tests/extra/pytest/test_dynlibs.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/extra/pytest/test_dynlibs.py b/tests/extra/pytest/test_dynlibs.py index 2081df18ac..8260c9af7a 100644 --- a/tests/extra/pytest/test_dynlibs.py +++ b/tests/extra/pytest/test_dynlibs.py @@ -28,15 +28,38 @@ def _count_enoent(subcommand: str, pattern: str, cwd: Path) -> int: - """Return the number of ENOENT lines matching `pattern` under strace.""" + """ + Return the number of ENOENT lines matching `pattern` under strace. + + Raises on strace failures (bad exit, timeout, empty stderr, or + stderr that lacks any syscall lines). Without these guards a + seccomp-restricted or ptrace_scope-restricted runner would produce + an empty stderr, a count of 0, and a vacuously passing test. + """ result = subprocess.run( ["strace", "-f", "git-annex", subcommand], cwd=cwd, capture_output=True, text=True, + timeout=120, ) + stderr = result.stderr + if result.returncode != 0: + raise RuntimeError( + f"strace exited {result.returncode} for `git-annex {subcommand}`:" + f"\n{stderr[-2000:]}" + ) + # Cheap sanity check: strace always emits at least a "+++ exited" + # and one syscall line if it actually ran. A blocked strace under + # seccomp / ptrace_scope produces essentially nothing on stderr. + if "+++ exited" not in stderr and " ENOENT " not in stderr and " = " not in stderr: + raise RuntimeError( + "strace produced no syscall output; is it blocked by seccomp / " + "ptrace_scope? Cannot trust ENOENT count.\n" + f"stderr head: {stderr[:2000]}" + ) regex = re.compile(rf"{pattern}.*ENOENT") - matches = [line for line in result.stderr.splitlines() if regex.search(line)] + matches = [line for line in stderr.splitlines() if regex.search(line)] for m in matches: print(m, file=sys.stderr) return len(matches) From bddf6793af29bb015e3b8d99c2f48e319ee51bc0 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 09:14:18 -0400 Subject: [PATCH 6/9] test_url_backend: Windows-safe teardown of the ReproTube clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewers #3/#4 flagged: the module-scoped `cloned_repo` fixture used `tmp_path_factory.mktemp` and returned the repo, leaving cleanup to pytest's own later `shutil.rmtree`. On Windows that fails because git-annex sets object files (and their containing key directories) to read-only; the same trap bites POSIX teardown when the containing directory is 0500 — locally reproduced with a `PermissionError` on unlinking `.git/annex/objects/vJ/G8/URL--yt...`. Fix: - Convert the fixture to `yield` + explicit teardown. - Walk the tree bottom-up and chmod every dir and file `u+rwx` before rmtree (mirrors `chmod -R u+w` from the dropped bats teardown). - Keep an rmtree `onexc` / `onerror` handler as a belt-and-braces fallback if a new read-only entry appears between the walk and the unlink. Uses `onexc` on Python 3.12+ and `onerror` on older Pythons (workflow pins 3.12, but local dev on 3.11 stays green). Local run: 4 passed; the parent `/tmp/pytest-of-USER/pytest-N/` directory is now empty on exit. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- tests/extra/pytest/test_url_backend.py | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/extra/pytest/test_url_backend.py b/tests/extra/pytest/test_url_backend.py index e59a560dcc..967006b6e9 100644 --- a/tests/extra/pytest/test_url_backend.py +++ b/tests/extra/pytest/test_url_backend.py @@ -19,8 +19,13 @@ from __future__ import annotations +import os +import shutil +import stat import subprocess +import sys from pathlib import Path +from typing import Any, Callable import pytest @@ -54,6 +59,37 @@ ) +def _make_tree_writable(root: Path) -> None: + """ + git-annex sets the key file *and* its containing directory to mode + 0500, which makes both `os.unlink(file)` and `os.rmdir(dir)` fail. + Walk the tree bottom-up and add owner-write to every dir and file + so a subsequent rmtree succeeds. Mirrors what `chmod -R u+w` did + in the dropped bats teardown. + """ + for dirpath, dirnames, filenames in os.walk(root): + for name in (*dirnames, *filenames): + p = os.path.join(dirpath, name) + try: + os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR) + except OSError: + pass + try: + os.chmod(root, os.stat(root).st_mode | stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR) + except OSError: + pass + + +def _chmod_and_retry(func: Callable[..., Any], path: str, _exc: BaseException) -> None: + """rmtree onexc fallback: chmod the file *and its parent dir* writable, retry.""" + for target in (path, os.path.dirname(path)): + try: + os.chmod(target, os.stat(target).st_mode | stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR) + except OSError: + pass + func(path) + + @pytest.fixture(scope="module") def cloned_repo(tmp_path_factory: pytest.TempPathFactory) -> Path: workdir = tmp_path_factory.mktemp("ReproTube") @@ -73,7 +109,21 @@ def cloned_repo(tmp_path_factory: pytest.TempPathFactory) -> Path: cwd=repo, check=True, ) subprocess.run(["git", "annex", "init", "-q"], cwd=repo, check=True) - return repo + yield repo + # Explicit teardown so pytest's later `tmp_path_factory` cleanup + # doesn't trip over git-annex's read-only object files (Windows, + # and also POSIX where the containing key-directory is 0500). + _make_tree_writable(workdir) + # Belt-and-braces: even after the walk, if a race added new + # read-only entries, the onexc handler chmods and retries. Python + # < 3.12 spells the kwarg `onerror`; 3.12+ prefers `onexc`. + if sys.version_info >= (3, 12): + shutil.rmtree(workdir, onexc=_chmod_and_retry) + else: + shutil.rmtree( + workdir, + onerror=lambda f, p, e: _chmod_and_retry(f, p, e[1]), + ) def test_whereis_parses_url_backend_key(cloned_repo: Path) -> None: From 012b0bfa51a600a20bed64db4bb84b223d599521 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 09:26:10 -0400 Subject: [PATCH 7/9] Colocate URL_BACKEND_FIX_VERSION with its only consumer Moved from tests/_helpers.py to tests/extra/pytest/test_url_backend.py so the fix-version constant, the DEP-3-style comment explaining WHY that specific version, and the xfail marker that uses it all live in one place. _helpers.py drops back to purely generic version-parsing utilities that make no assumption about which bug is being probed. No behaviour change; 4/4 pytest tests still pass. Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- tests/_helpers.py | 7 ------- tests/extra/pytest/test_url_backend.py | 7 ++----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/_helpers.py b/tests/_helpers.py index a38dbaf0fb..0a7e30990c 100644 --- a/tests/_helpers.py +++ b/tests/_helpers.py @@ -18,13 +18,6 @@ from functools import lru_cache -# Fix version for the URL-encoded-key bug (upstream 8fd9b67ed8, -# 2026-02-16; first shipped in release 10.20260420). Single source of -# truth for both the xfail marker in test_url_backend.py and the -# version reporting in tests/conftest.py. -URL_BACKEND_FIX_VERSION = "10.20260420" - - @lru_cache(maxsize=1) def git_annex_version_output() -> str | None: """Raw stdout of `git annex version`, cached for the session.""" diff --git a/tests/extra/pytest/test_url_backend.py b/tests/extra/pytest/test_url_backend.py index 967006b6e9..31f852d3b2 100644 --- a/tests/extra/pytest/test_url_backend.py +++ b/tests/extra/pytest/test_url_backend.py @@ -29,17 +29,14 @@ import pytest -from _helpers import ( - URL_BACKEND_FIX_VERSION, - git_annex_version, - git_annex_version_below, -) +from _helpers import git_annex_version, git_annex_version_below # URL_BACKEND_FIX_VERSION is the git-annex release that first shipped # the fix (upstream commit 8fd9b67ed8 "factor out extendUrlWithPath …", # 2026-02-16). Older versions xfail so we do not block CI on a known # regression while still guaranteeing that once a build is on a fixed # version, the test acts as a permanent regression guard. +URL_BACKEND_FIX_VERSION = "10.20260420" _xfail_broken_url_backend = pytest.mark.xfail( condition=git_annex_version_below(URL_BACKEND_FIX_VERSION), From 64590ab264aaec7e4d0d60f9181d632705984e58 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 09:33:18 -0400 Subject: [PATCH 8/9] Forbid xfails on CI so failures are always loud MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On CI we build a specific git-annex and run against it: every failure is signal. The xfail(strict=False) marker on test_get_url_backend_key was designed for the case "local dev on an older branch where the bug is known" — silently accepting the failure keeps CI reviewers focused on real regressions. That trade-off doesn't apply on CI itself: there, an XFAIL is a hidden failure. Fix: gate the xfail condition on `not os.environ.get("CI")`. In concrete terms: - Locally, xfail engages when installed git-annex < fix version, as before. Silent XFAIL / XPASS on older versions; loud FAIL / PASS on current versions. - On CI (CI env var set — GitHub Actions, Travis, GitLab, and every other standard runner set this), the xfail marker is inert. A regression that happens to hit an old version on CI (e.g. a workflow_dispatch of a pre-fix commitish) will red the run — which is what we want, because CI's job is to say pass or fail on the exact build under test. Verified: `pytest tests` and `CI=1 pytest tests` both green locally on 10.20260421 (post-fix, xfail inert either way). Co-Authored-By: Claude Code 2.1.220 / Claude Opus 4.7 (1M context) --- tests/extra/pytest/test_url_backend.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/extra/pytest/test_url_backend.py b/tests/extra/pytest/test_url_backend.py index 31f852d3b2..1eb8ca952b 100644 --- a/tests/extra/pytest/test_url_backend.py +++ b/tests/extra/pytest/test_url_backend.py @@ -33,13 +33,22 @@ # URL_BACKEND_FIX_VERSION is the git-annex release that first shipped # the fix (upstream commit 8fd9b67ed8 "factor out extendUrlWithPath …", -# 2026-02-16). Older versions xfail so we do not block CI on a known -# regression while still guaranteeing that once a build is on a fixed -# version, the test acts as a permanent regression guard. +# 2026-02-16). Older versions xfail so we do not block local dev on +# a known regression while still guaranteeing that once a build is on +# a fixed version, the test acts as a permanent regression guard. URL_BACKEND_FIX_VERSION = "10.20260420" +# On CI, forbid xfails: CI runs against a specific build of git-annex, +# and we want every failure — including "known-broken old-version" +# failures — to be loud rather than silently swallowed by an xfail +# marker. Setting condition=False disables the xfail entirely (so a +# failure surfaces as a normal FAIL), independent of the installed +# version. Locally, the version check keeps the marker useful for +# interactive dev on older branches. +_ON_CI = bool(os.environ.get("CI")) + _xfail_broken_url_backend = pytest.mark.xfail( - condition=git_annex_version_below(URL_BACKEND_FIX_VERSION), + condition=(not _ON_CI) and git_annex_version_below(URL_BACKEND_FIX_VERSION), reason=( f"URL-encoded-key retrieval bug present in git-annex " f"< {URL_BACKEND_FIX_VERSION} " From 32003865682d1aedc16a1d4f6b0d9cdfffe9cf07 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 3 Aug 2026 10:18:51 -0400 Subject: [PATCH 9/9] [DATALAD RUNCMD] Regenerate workflows for macOS ARM64 support === Do not change lines below === { "chain": [ "024459120c9868becff366655e3e5bc1533dddae" ], "cmd": "make -C .github/workflows/template", "exit": 0, "extra_inputs": [], "inputs": [], "outputs": [], "pwd": "." } ^^^ Do not change lines above ^^^ --- .github/workflows/build-macos-arm64.yaml | 12 ++++++------ .github/workflows/build-macos.yaml | 12 ++++++------ .github/workflows/build-ubuntu.yaml | 12 ++++++------ .github/workflows/build-windows.yaml | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-macos-arm64.yaml b/.github/workflows/build-macos-arm64.yaml index d660ccff9a..db6f0c6d1d 100644 --- a/.github/workflows/build-macos-arm64.yaml +++ b/.github/workflows/build-macos-arm64.yaml @@ -134,7 +134,7 @@ jobs: git-annex_"${{ steps.build-version.outputs.version }}".dmg - name: Upload packages - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: git-annex-macos-arm64-dmg_${{ steps.build-version.outputs.version }} path: | @@ -182,7 +182,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -223,7 +223,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-macos-arm64-dmg_${{ needs.build-package.outputs.build-version }} @@ -324,7 +324,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -363,7 +363,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-macos-arm64-dmg_${{ needs.build-package.outputs.build-version }} @@ -426,7 +426,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-macos-arm64-dmg_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/build-macos.yaml b/.github/workflows/build-macos.yaml index f3ccd098ec..47017c5ef2 100644 --- a/.github/workflows/build-macos.yaml +++ b/.github/workflows/build-macos.yaml @@ -134,7 +134,7 @@ jobs: git-annex_"${{ steps.build-version.outputs.version }}".dmg - name: Upload packages - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: git-annex-macos-dmg_${{ steps.build-version.outputs.version }} path: | @@ -182,7 +182,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -222,7 +222,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-macos-dmg_${{ needs.build-package.outputs.build-version }} @@ -323,7 +323,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -362,7 +362,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-macos-dmg_${{ needs.build-package.outputs.build-version }} @@ -425,7 +425,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-macos-dmg_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/build-ubuntu.yaml b/.github/workflows/build-ubuntu.yaml index 74f439116a..c7b3a54fe1 100644 --- a/.github/workflows/build-ubuntu.yaml +++ b/.github/workflows/build-ubuntu.yaml @@ -142,7 +142,7 @@ jobs: if: "!contains(env.DEB_BUILD_OPTIONS, 'nocheck')" - name: Upload packages - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: git-annex-debianstandalone-packages_${{ steps.build-version.outputs.version }} path: | @@ -226,7 +226,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -266,7 +266,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-debianstandalone-packages_${{ needs.build-package.outputs.build-version }} @@ -361,7 +361,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -400,7 +400,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-debianstandalone-packages_${{ needs.build-package.outputs.build-version }} @@ -462,7 +462,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-debianstandalone-packages_${{ needs.build-package.outputs.build-version }} diff --git a/.github/workflows/build-windows.yaml b/.github/workflows/build-windows.yaml index 5f986d6ef8..dc586cc803 100644 --- a/.github/workflows/build-windows.yaml +++ b/.github/workflows/build-windows.yaml @@ -154,7 +154,7 @@ jobs: git-annex-installer_"${{ steps.build-version.outputs.version }}".exe - name: Upload packages - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: git-annex-windows-installer_${{ steps.build-version.outputs.version }} path: | @@ -202,7 +202,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -242,7 +242,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-windows-installer_${{ needs.build-package.outputs.build-version }} @@ -309,7 +309,7 @@ jobs: - name: Send e-mail on failed run if: failure() && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) # freeze to v17 - uses: dawidd6/action-send-mail@6e71c855c9a091d80a519621b9fd3e8d252ca40c + uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c with: server_address: ${{ secrets.NOTIFY_SMTP_HOST }} server_port: ${{ secrets.NOTIFY_SMTP_PORT }} @@ -348,7 +348,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-windows-installer_${{ needs.build-package.outputs.build-version }} @@ -409,7 +409,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download git-annex package - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: git-annex-windows-installer_${{ needs.build-package.outputs.build-version }}