diff --git a/.gitea/workflows/pr-title-check.yml b/.gitea/workflows/pr-title-check.yml index ba94db6..efb358b 100644 --- a/.gitea/workflows/pr-title-check.yml +++ b/.gitea/workflows/pr-title-check.yml @@ -16,8 +16,14 @@ jobs: - name: Check PR title when source files change env: - PR_TITLE: ${{ github.event.pull_request.title }} + API_URL: ${{ vars.API_URL }} + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }} run: | git fetch origin "${{ github.base_ref }}" + PR_TITLE=$(scripts/ci/fetch-pr-title.sh) + echo "Live PR title: ${PR_TITLE}" git diff --name-only "origin/${{ github.base_ref }}...HEAD" \ | scripts/ci/check-pr-title.sh "$PR_TITLE" diff --git a/python_repositories/adapters/minio_adapter.py b/python_repositories/adapters/minio_adapter.py index 02775af..1f3cb1e 100644 --- a/python_repositories/adapters/minio_adapter.py +++ b/python_repositories/adapters/minio_adapter.py @@ -152,6 +152,7 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter): assert self._client is not None and self._bucket_name is not None # Get data from bucket # N.B. bucket name is set when connecting + response = None try: response = self._client.get_object( bucket_name=self._bucket_name, @@ -175,6 +176,10 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter): self.logger.error(repr(exc)) except Exception as exc: # pylint: disable=broad-except self.logger.error(repr(exc)) + finally: + if response is not None: + response.close() + response.release_conn() return None def delete(self, object_name: str) -> None: diff --git a/scripts/ci/fetch-pr-title.sh b/scripts/ci/fetch-pr-title.sh new file mode 100755 index 0000000..29f95e2 --- /dev/null +++ b/scripts/ci/fetch-pr-title.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fetch the current PR title from the Gitea API. +# Requires: API_URL, REPO_OWNER, REPO_NAME, PR_NUMBER, CI_RUNNER_TOKEN + +: "${API_URL:?API_URL is required}" +: "${REPO_OWNER:?REPO_OWNER is required}" +: "${REPO_NAME:?REPO_NAME is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${CI_RUNNER_TOKEN:?CI_RUNNER_TOKEN is required}" + +curl -sf \ + "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}" \ + -H "Authorization: token ${CI_RUNNER_TOKEN}" \ + | python3 -c 'import json, sys; print(json.load(sys.stdin)["title"])' diff --git a/tests/unit/minio_adapter_test.py b/tests/unit/minio_adapter_test.py index 5e588e9..5dbb18b 100644 --- a/tests/unit/minio_adapter_test.py +++ b/tests/unit/minio_adapter_test.py @@ -91,3 +91,36 @@ def test_connect_disconnects_before_reconnect( assert adapter._client is new_client new_client.list_buckets.assert_called_once() + + +def test_get_closes_response_on_success() -> None: + """get() must close and release the get_object HTTP response.""" + mock_client = MagicMock(spec=Minio) + mock_client.bucket_exists.return_value = True + mock_response = MagicMock() + mock_response.read.side_effect = [b"data", b""] + mock_client.get_object.return_value = mock_response + adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) + + result = adapter.get("some-object") + + assert result is not None + assert result.read() == b"data" + mock_response.close.assert_called_once() + mock_response.release_conn.assert_called_once() + + +def test_get_closes_response_when_read_fails() -> None: + """get() must close and release the response even if read() raises.""" + mock_client = MagicMock(spec=Minio) + mock_client.bucket_exists.return_value = True + mock_response = MagicMock() + mock_response.read.side_effect = OSError("connection reset") + mock_client.get_object.return_value = mock_response + adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) + + result = adapter.get("some-object") + + assert result is None + mock_response.close.assert_called_once() + mock_response.release_conn.assert_called_once()