diff --git a/pullnrun/_log.py b/pullnrun/_log.py
index 89198bd..02bf8c3 100644
--- a/pullnrun/_log.py
+++ b/pullnrun/_log.py
@@ -1,18 +1,47 @@
+def _http_details(output_dict):
+    type_ = output_dict.get('type', '')
+    status = str(output_dict.get('data', {}).get('status', '')).rjust(4)
+
+    file_ = output_dict.get('data', {}).get('file', '')
+    direction = 'to' if 'push' in type_ else 'from'
+    url = output_dict.get('data', {}).get('url', '')
+
+    detail = f'{file_} {direction} {url}'
+
+    return (status, detail, )
+
+def _s3_details(output_dict):
+    type_ = output_dict.get('type', '')
+
+    filename = output_dict.get('data', {}).get('filename', '')
+    bucket = output_dict.get('data', {}).get('bucket', '')
+    object_name = output_dict.get('data', {}).get('object_name', '')
+
+    if 'push' in type_:
+        direction = 'to'
+        source, target = filename, object_name
+    else:
+        direction = 'from'
+        source, target = object_name, filename
+
+    target = f' as {target}' if source != target else ''
+    detail = f'{source} {direction} S3 bucket {bucket}{target}'
+
+    return detail
+
 def log_to_console(output_dict):
     ok = '\u2714' if output_dict.get('ok') else '\u2718'
     type_ = output_dict.get('type', '')
-    stage = type_.upper().ljust(4)
+    stage = type_.upper()[:4].ljust(4)
 
     status = ''.rjust(4)
     detail = ''
     output = None
 
     if type_ in ('pull_http', 'push_http'):
-        status = str(output_dict.get('data', {}).get('status', '')).rjust(4)
-        file_ = output_dict.get('data', {}).get('file', '')
-        url = output_dict.get('data', {}).get('url', '')
-        direction = 'to' if 'push' in type_ else 'from'
-        detail = f'{file_} {direction} {url}'
+        status, detail = _http_details(output_dict)
+    elif type_ in ('pull_s3', 'push_s3'):
+        detail = _s3_details(output_dict)
     elif type_ == 'run':
         status = str(output_dict.get('data', {}).get('exit_code', '')).rjust(4)
         detail = ' '.join(output_dict.get('data', {}).get('command', []))

Add details logging for S3 pull and push
diff --git a/tst/test_log.py b/tst/test_log.py
index 596d35c..ed94950 100644
--- a/tst/test_log.py
+++ b/tst/test_log.py
@@ -10,18 +10,45 @@ class LogTest(TestCase):
         print_mock.assert_called()
 
     @patch('builtins.print')
-    def test_log_to_console_prints_type_and_status(self, print_mock):
+    def test_log_to_console_prints_logs_as_specified(self, print_mock):
+        url = 'example.com'
+        f = 'filename'
+
         testdata = [
-            ({'type': 'pull_http', 'ok': True, 'data': {'status': 200}}, ['\u2714', '200', 'PULL']),
-            ({'type': 'run', 'ok': False, 'data': {'exit_code': 200}}, ['\u2718', '200', 'RUN']),
+            # Type and status
+            ({'type': 'pull_http', 'data': {
+                'file': f,
+                'url': url,
+            }}, [f, 'from', url], []),
+            ({'type': 'push_http', 'data': {
+                'file': f,
+                'url': url,
+            }}, [f, 'to', url], []),
+            # HTTP details
+            ({'type': 'pull_http', 'ok': True, 'data': {'status': 200}}, ['\u2714', '200', 'PULL'], ['_HTTP']),
+            ({'type': 'run', 'ok': False, 'data': {'exit_code': 200}}, ['\u2718', '200', 'RUN'], ['_HTTP']),
+            # S3 details
+            ({'type': 'pull_s3', 'data': {
+                'bucket': 'b',
+                'object_name': 'a',
+                'filename': 'c',
+            }}, ['PULL', 'a from S3 bucket b as c'], ['_S3']),
+            ({'type': 'push_s3', 'data': {
+                'bucket': 'b',
+                'object_name': 'a',
+                'filename': 'a',
+            }}, ['PUSH', 'a to S3 bucket b'], ['as a', '_S3']),
         ]
 
-        for data, results in testdata:
+        for data, p_results, n_results in testdata:
             log_to_console(data)
             output = print_mock.call_args[-2][0]
 
-            for result in results:
+            for result in p_results:
                 self.assertIn(result, output)
+            for result in n_results:
+                self.assertNotIn(result, output)
+
 
     @patch('builtins.print')
     def test_log_to_console_prints_run_output(self, print_mock):
@@ -32,20 +59,3 @@ class LogTest(TestCase):
         output = print_mock.call_args[-2][0]
 
         self.assertIn(result, output)
-
-    @patch('builtins.print')
-    def test_log_to_console_prints_pull_and_push_url(self, print_mock):
-        url = 'example.com'
-        f = 'filename'
-
-        testdata = [
-            ({'type': 'pull_http', 'data': {'file': f, 'url': url}}, [f, 'from', url]),
-            ({'type': 'push_http', 'data': {'file': f, 'url': url}}, [f, 'to', url]),
-        ]
-
-        for data, results in testdata:
-            log_to_console(data)
-            output = print_mock.call_args[-2][0]
-
-            for result in results:
-                self.assertIn(result, output)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
astroid==2.15.8
attrs==24.2.0
boto3==1.33.13
botocore==1.33.13
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
coverage==7.2.7
dill==0.3.7
exceptiongroup==1.2.2
idna==3.10
importlib-metadata==6.7.0
importlib-resources==5.12.0
iniconfig==2.0.0
isort==5.11.5
jmespath==1.0.1
jsonschema==4.17.3
lazy-object-proxy==1.9.0
mccabe==0.7.0
packaging==24.0
pkgutil_resolve_name==1.3.10
platformdirs==4.0.0
pluggy==1.2.0
-e git+https://github.com/kangasta/pullnrun.git@e5a9e4f337915ad48f0aaaac04956c56220a4d56#egg=pullnrun
pylint==2.17.7
pyrsistent==0.19.3
pytest==7.4.4
python-dateutil==2.9.0.post0
requests==2.31.0
s3transfer==0.8.2
six==1.17.0
swebench-matterhorn @ file:///swebench_matterhorn
tomli==2.0.1
tomlkit==0.12.5
typed-ast==1.5.5
typing_extensions==4.7.1
urllib3==1.26.20
wrapt==1.16.0
zipp==3.15.0

name: pullnrun
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - astroid==2.15.8
      - attrs==24.2.0
      - boto3==1.33.13
      - botocore==1.33.13
      - charset-normalizer==3.4.1
      - coverage==7.2.7
      - dill==0.3.7
      - exceptiongroup==1.2.2
      - idna==3.10
      - importlib-metadata==6.7.0
      - importlib-resources==5.12.0
      - iniconfig==2.0.0
      - isort==5.11.5
      - jmespath==1.0.1
      - jsonschema==4.17.3
      - lazy-object-proxy==1.9.0
      - mccabe==0.7.0
      - packaging==24.0
      - pkgutil-resolve-name==1.3.10
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pylint==2.17.7
      - pyrsistent==0.19.3
      - pytest==7.4.4
      - python-dateutil==2.9.0.post0
      - requests==2.31.0
      - s3transfer==0.8.2
      - six==1.17.0
      - swebench-matterhorn==0.0.0
      - tomli==2.0.1
      - tomlkit==0.12.5
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - urllib3==1.26.20
      - wrapt==1.16.0
      - zipp==3.15.0
prefix: /opt/conda/envs/pullnrun

tst/test_log.py::LogTest::test_log_to_console_prints_logs_as_specified
tst/test_log.py::LogTest::test_log_to_console_does_not_crash_on_empty_input tst/test_log.py::LogTest::test_log_to_console_prints_run_output
pullnrun/_log.py
pullnrun/_log.py