diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfb5bbb..6c256f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: matrix: python-version: ["3.10", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -26,8 +26,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install build tools @@ -55,8 +55,8 @@ jobs: id-token: write contents: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install build tools @@ -71,10 +71,12 @@ jobs: id: changelog run: | VERSION="${GITHUB_REF_NAME#swoop-v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" # Extract the section between this version's header and the next version header awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md > release-notes.md cat release-notes.md - name: Create GitHub Release - run: gh release create "${{ github.ref_name }}" --notes-file release-notes.md + run: gh release create "${{ github.ref_name }}" --title "v${VERSION}" --notes-file release-notes.md env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.changelog.outputs.version }} diff --git a/.github/workflows/live-canary.yml b/.github/workflows/live-canary.yml index 2ec2640..3c540f8 100644 --- a/.github/workflows/live-canary.yml +++ b/.github/workflows/live-canary.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies @@ -24,7 +24,7 @@ jobs: run: python -m pytest tests/test_live_contract.py -v -m live - name: Upload live canary artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: live-canary-artifacts path: ${{ runner.temp }}/swoop-live-artifacts diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index ba52ddf..d6510a9 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -7,8 +7,8 @@ jobs: mutmut: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies @@ -21,7 +21,7 @@ jobs: run: mutmut results > mutmut-results.txt || true - name: Upload mutation results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: mutmut-results path: mutmut-results.txt diff --git a/swoop/rpc.py b/swoop/rpc.py index c807c39..1a08ec7 100644 --- a/swoop/rpc.py +++ b/swoop/rpc.py @@ -621,11 +621,18 @@ def get_booking_results( transport=transport, ) - return _parse_booking_rpc_response( + options = _parse_booking_rpc_response( res.text, registry_version=registry_version, required_keys=required_keys, ) + if not options: + logger.warning( + "get_booking_results %s->%s on %s returned 0 options " + "(upstream RPC succeeded; token may be stale or itinerary unbookable)", + origin, destination, date, + ) + return options def get_trip_booking_results( diff --git a/tests/test_live_contract.py b/tests/test_live_contract.py index b2eca00..233bbf4 100644 --- a/tests/test_live_contract.py +++ b/tests/test_live_contract.py @@ -235,15 +235,18 @@ def _record_booking_artifacts(case_id: str, rpc_captures: list[dict[str, str]], ) -def _find_bookable_itinerary(search_result: Any) -> Itinerary: +def _bookable_candidates(search_result: Any, *, limit: int = 5) -> list[Itinerary]: + candidates: list[Itinerary] = [] for option in search_result.results: for leg in option.legs: itinerary = leg.itinerary if itinerary is None: continue if itinerary.booking_token and rpc._build_selected_legs(itinerary): - return itinerary - raise AssertionError("Expected at least one itinerary with booking token and selected legs") + candidates.append(itinerary) + if len(candidates) >= limit: + return candidates + return candidates class TestShoppingContract: @@ -317,16 +320,27 @@ def test_booking_results_parseable_and_artifacted(self, monkeypatch: pytest.Monk ) assert search_result.results, "Expected at least one itinerary before live booking lookup" - itinerary = _find_bookable_itinerary(search_result) + candidates = _bookable_candidates(search_result) + assert candidates, "Expected at least one itinerary with booking token and selected legs" + rpc_captures = _capture_rpc_texts(monkeypatch) - options = rpc.get_booking_results( - itinerary, - registry_version=date.today().isoformat(), - transport=TransportConfig(timeout=30, retries=1), - ) + options: list[Any] = [] + itinerary: Itinerary | None = None + for candidate in candidates: + options = rpc.get_booking_results( + candidate, + registry_version=date.today().isoformat(), + transport=TransportConfig(timeout=30, retries=1), + ) + if options: + itinerary = candidate + break assert rpc_captures, "Expected a live booking RPC capture" - assert options, "Expected at least one booking option from live booking lookup" + assert options and itinerary is not None, ( + f"Expected at least one booking option after trying {len(candidates)} " + "itineraries (upstream returned empty for every candidate)" + ) for option in options[:5]: assert option.price > 0