@@ -34,27 +34,27 @@ concurrency:
3434 cancel-in-progress : true
3535
3636jobs :
37-
3837 pre-commit :
3938 runs-on : ubuntu-latest
4039 steps :
41- - uses : actions/checkout@v4
42- - uses : actions/setup-python@v5
43- with :
44- python-version : ' 3.13'
45- - uses : pre-commit/action@v3.0.1
40+ - uses : actions/checkout@v4
41+ - uses : actions/setup-python@v5
42+ with :
43+ python-version : ' 3.13'
44+ - uses : pre-commit/action@v3.0.1
4645
4746 # run unit (and integration tests if account secrets available) on our build matrix
4847 test :
4948 needs : [pre-commit]
5049
5150 strategy :
51+ fail-fast : false
5252 matrix :
5353 os : [ubuntu-22.04, macos-13, windows-2022]
5454
5555 # if changing the below change the run-integration-tests versions and the check-deploy versions
5656 # Make sure that we are running the integration tests on the first and last versions of the matrix
57- python : ['3.9 ', '3.10 ', '3.11 ', '3.12 ', '3.13 ']
57+ python : ['3.10 ', '3.11 ', '3.12 ', '3.13 ', '3.14 ']
5858
5959 runs-on : ${{ matrix.os }}
6060
@@ -83,15 +83,15 @@ jobs:
8383 path : |
8484 ${{ steps.get-dependencies.outputs.site_packages_loc }}
8585 ${{ steps.get-dependencies.outputs.site_bin_dir }}
86- key : ${{ runner.os }}-${{ matrix.python }}-build-${{ env.cache-name }}-${{ hashFiles('setup.py') }}-v24
86+ key : ${{ runner.os }}-${{ matrix.python }}-build-${{ env.cache-name }}-${{ hashFiles('setup.py') }}-v29
8787
8888 - name : Install py-dependencies
8989 if : steps.cache-dependencies.outputs.cache-hit != 'true'
9090 shell : bash
9191 run : |
9292 python -m pip install --upgrade pip
9393
94- pip install -e ".[boto3,pandas,pysftp,tests]"
94+ pip install -e ".[boto3,pandas,pysftp,tests,curator ]"
9595
9696 # ensure that numpy c extensions are installed on windows
9797 # https://stackoverflow.com/a/59346525
@@ -108,7 +108,7 @@ jobs:
108108 pytest -sv --cov-append --cov=. --cov-report xml tests/unit
109109 - name : Check for Secret availability
110110 id : secret-check
111- if : ${{ contains(fromJSON('["3.9 "]'), matrix.python) || contains(fromJSON('["3.13 "]'), matrix.python) }}
111+ if : ${{ contains(fromJSON('["3.10 "]'), matrix.python) || contains(fromJSON('["3.14 "]'), matrix.python) }}
112112 # perform secret check & put boolean result as an output
113113 shell : bash
114114 run : |
@@ -124,17 +124,48 @@ jobs:
124124 echo "synapse_pat_available=true" >> $GITHUB_OUTPUT;
125125 fi
126126
127+ - name : Download Failed Tests from Previous Attempt
128+ if : ${{ github.run_attempt > 1 && (contains(fromJSON('["3.10"]'), matrix.python) || contains(fromJSON('["3.14"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
129+ uses : actions/download-artifact@v4
130+ with :
131+ name : failed-tests-${{ matrix.os }}-${{ matrix.python }}
132+ continue-on-error : true
133+
127134 # run integration tests iff the decryption keys for the test configuration are available.
128135 # they will not be available in pull requests from forks.
129136 # run integration tests on the oldest and newest supported versions of python.
130137 # we don't run on the entire matrix to avoid a 3xN set of concurrent tests against
131138 # the target server where N is the number of supported python versions.
132139 - name : run-integration-tests
140+ id : integration_tests
133141 shell : bash
134142
135143 # keep versions consistent with the first and last from the strategy matrix
136- if : ${{ (contains(fromJSON('["3.9 "]'), matrix.python) || contains(fromJSON('["3.13 "]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
144+ if : ${{ (contains(fromJSON('["3.10 "]'), matrix.python) || contains(fromJSON('["3.14 "]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
137145 run : |
146+ # Set SYNAPSE_PROFILE based on OS and Python version
147+ if [ "${{ startsWith(matrix.os, 'ubuntu') }}" == "true" ]; then
148+ if [ "${{ matrix.python }}" == "3.10" ]; then
149+ export SYNAPSE_PROFILE="TestUbuntuMinimumPython"
150+ elif [ "${{ matrix.python }}" == "3.14" ]; then
151+ export SYNAPSE_PROFILE="TestUbuntuMaximumPython"
152+ fi
153+ elif [ "${{ startsWith(matrix.os, 'windows') }}" == "true" ]; then
154+ if [ "${{ matrix.python }}" == "3.10" ]; then
155+ export SYNAPSE_PROFILE="TestWindowsMinimumPython"
156+ elif [ "${{ matrix.python }}" == "3.14" ]; then
157+ export SYNAPSE_PROFILE="TestWindowsMaximumPython"
158+ fi
159+ elif [ "${{ startsWith(matrix.os, 'macos') }}" == "true" ]; then
160+ if [ "${{ matrix.python }}" == "3.10" ]; then
161+ export SYNAPSE_PROFILE="TestMacosMinimumPython"
162+ elif [ "${{ matrix.python }}" == "3.14" ]; then
163+ export SYNAPSE_PROFILE="TestMacosMaximumPython"
164+ fi
165+ fi
166+
167+ echo "Using SYNAPSE_PROFILE: $SYNAPSE_PROFILE"
168+
138169 # decrypt the encrypted test synapse configuration
139170 openssl aes-256-cbc -K ${{ secrets.encrypted_d17283647768_key }} -iv ${{ secrets.encrypted_d17283647768_iv }} -in test.synapseConfig.enc -out test.synapseConfig -d
140171 mv test.synapseConfig ~/.synapseConfig
@@ -170,21 +201,96 @@ jobs:
170201 # Setup ignore patterns based on Python version
171202 IGNORE_FLAGS="--ignore=tests/integration/synapseclient/test_command_line_client.py"
172203
173- if [ "${{ matrix.python }}" == "3.9" ]; then
174- # For min Python version, ignore async tests
175- IGNORE_FLAGS="$IGNORE_FLAGS --ignore=tests/integration/synapseclient/models/async/"
176- echo "Running integration tests for Min Python version (3.9) - ignoring async tests"
177- elif [ "${{ matrix.python }}" == "3.13" ]; then
178- # For max Python version, ignore synchronous tests
179- IGNORE_FLAGS="$IGNORE_FLAGS --ignore=tests/integration/synapseclient/models/synchronous/"
180- echo "Running integration tests for Max Python version (3.13) - ignoring synchronous tests"
181- fi
204+ # Check if we should run only failed tests from previous attempt
205+ if [[ -f failed_tests.txt && -s failed_tests.txt ]]; then
206+ echo "::notice::Retry attempt ${{ github.run_attempt }} detected - running only previously failed tests"
207+ cat failed_tests.txt
182208
183- # use loadscope to avoid issues running tests concurrently that share scoped fixtures
184- pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml tests/integration -n 8 $IGNORE_FLAGS --dist loadscope
209+ # Run only the failed tests
210+ pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml \
211+ --junit-xml=test-results.xml \
212+ -n 8 --dist loadscope \
213+ $(cat failed_tests.txt | tr '\n' ' ')
214+ else
215+ echo "::notice::First attempt or no previous failures - running full integration test suite"
216+
217+ # use loadscope to avoid issues running tests concurrently that share scoped fixtures
218+ pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml \
219+ --junit-xml=test-results.xml \
220+ tests/integration -n 8 $IGNORE_FLAGS --dist loadscope
221+ fi
185222
186223 # Execute the CLI tests in a non-dist way because they were causing some test instability when being run concurrently
187224 pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml tests/integration/synapseclient/test_command_line_client.py
225+
226+ - name : Extract Failed Tests
227+ if : always() && steps.integration_tests.outcome == 'failure'
228+ shell : bash
229+ run : |
230+ python -c "
231+ import xml.etree.ElementTree as ET
232+ import os
233+ tree = ET.parse('test-results.xml')
234+ root = tree.getroot()
235+ failed = []
236+ for testcase in root.iter('testcase'):
237+ if testcase.find('failure') is not None or testcase.find('error') is not None:
238+ classname = testcase.get('classname')
239+ name = testcase.get('name')
240+ file_attr = testcase.get('file')
241+
242+ # Use the file attribute if available, otherwise convert classname
243+ if file_attr:
244+ # file attribute is already in the correct format
245+ test_path = f'{file_attr}::{classname.split(\".\")[-1]}::{name}'
246+ else:
247+ # Convert classname from dot notation to file path
248+ # e.g., 'tests.integration.foo.test_bar.TestClass' -> 'tests/integration/foo/test_bar.py::TestClass'
249+ parts = classname.split('.')
250+ # Find the test file (usually starts with 'test_')
251+ module_parts = []
252+ class_parts = []
253+ found_test_file = False
254+ for part in parts:
255+ if not found_test_file:
256+ module_parts.append(part)
257+ if part.startswith('test_'):
258+ found_test_file = True
259+ else:
260+ class_parts.append(part)
261+
262+ file_path = '/'.join(module_parts) + '.py'
263+ if class_parts:
264+ test_path = f'{file_path}::{\"::\".join(class_parts)}::{name}'
265+ else:
266+ test_path = f'{file_path}::{name}'
267+
268+ failed.append(test_path)
269+
270+ with open('failed_tests.txt', 'w') as f:
271+ f.write('\n'.join(failed))
272+ print(f'Found {len(failed)} failed tests')
273+ for test in failed:
274+ print(f' - {test}')
275+ print(f'Current attempt: ${{ github.run_attempt }}')
276+ "
277+
278+ - name : Upload Failed Tests for Next Attempt
279+ if : always() && steps.integration_tests.outcome == 'failure' && github.run_attempt < 3
280+ uses : actions/upload-artifact@v4
281+ with :
282+ name : failed-tests-${{ matrix.os }}-${{ matrix.python }}
283+ path : failed_tests.txt
284+ retention-days : 2
285+ overwrite : true
286+
287+ - name : Fail job if integration tests failed after all retries
288+ if : always() && steps.integration_tests.outcome == 'failure'
289+ shell : bash
290+ run : |
291+ echo "::error::Integration tests failed after ${{ github.run_attempt }} attempt(s)"
292+ exit 1
293+
188294 - name : Upload coverage report
189295 id : upload_coverage_report
190296 uses : actions/upload-artifact@v4
@@ -201,12 +307,12 @@ jobs:
201307 steps :
202308 - uses : actions/checkout@v4
203309 with :
204- fetch-depth : 0 # Shallow clones should be disabled for a better relevancy of analysis
310+ fetch-depth : 0 # Shallow clones should be disabled for a better relevancy of analysis
205311 - name : Check coverage-report artifact existence
206312 id : check_coverage_report
207313 uses : LIT-Protocol/artifact-exists-action@v0
208314 with :
209- name : " coverage-report"
315+ name : ' coverage-report'
210316 - name : Download coverage report
211317 uses : actions/download-artifact@v4
212318 if : steps.check_coverage_report.outputs.exists == 'true'
@@ -216,21 +322,21 @@ jobs:
216322 id : check_coverage_xml
217323 uses : andstor/file-existence-action@v3
218324 with :
219- files : " coverage.xml"
325+ files : ' coverage.xml'
220326 # This is a workaround described in https://community.sonarsource.com/t/sonar-on-github-actions-with-python-coverage-source-issue/36057
221327 - name : Override Coverage Source Path for Sonar
222328 if : steps.check_coverage_xml.outputs.files_exists == 'true'
223329 run : sed -i "s/<source>\/home\/runner\/work\/synapsePythonClient<\/source>/<source>\/github\/workspace<\/source>/g" coverage.xml
224330 - name : SonarCloud Scan
225- uses : SonarSource/sonarcloud-github -action@master
331+ uses : SonarSource/sonarqube-scan -action@v5.3.1
226332 if : ${{ always() }}
227333 env :
228334 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
229335 SONAR_TOKEN : ${{ secrets.SONAR_TOKEN }}
230336
231337 # on a GitHub release, build the pip package and upload it as a GitHub release asset
232338 package :
233- needs : [test,pre-commit]
339+ needs : [test, pre-commit]
234340
235341 runs-on : ubuntu-22.04
236342
@@ -245,7 +351,7 @@ jobs:
245351
246352 - uses : actions/setup-python@v5
247353 with :
248- python-version : 3.9
354+ python-version : 3.10
249355
250356 - name : set-release-env
251357 shell : bash
@@ -342,7 +448,6 @@ jobs:
342448 # asset_path: dist/${{ steps.build-package.outputs.bdist-package-name }}
343449 # asset_content_type: application/zip
344450
345-
346451 # build standalone desktop client artifacts for Windows and macOS on release
347452 build-electron-desktop-clients :
348453 needs : [test, pre-commit]
@@ -482,7 +587,7 @@ jobs:
482587 os : [ubuntu-24.04, macos-13, windows-2022]
483588
484589 # python versions should be consistent with the strategy matrix and the runs-integration-tests versions
485- python : ['3.9 ', '3.10 ', '3.11 ', '3.12 ', '3.13 ']
590+ python : ['3.10 ', '3.11 ', '3.12 ', '3.13 ', '3.14 ']
486591
487592 runs-on : ${{ matrix.os }}
488593
0 commit comments