This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-releases-client.git


The following commit(s) were added to refs/heads/main by this push:
     new 6a80957  Add workflow tests and fix a JWT refresh bug
6a80957 is described below

commit 6a809570e39e8928da493d22cc8f277b81124d6c
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jul 10 20:46:43 2025 +0100

    Add workflow tests and fix a JWT refresh bug
---
 pyproject.toml          |  4 ++--
 src/atrclient/client.py | 51 ++++++++++++++++++++++++++++++++++++++-----------
 tests/cli_workflow.t    | 20 +++++++++++++++++++
 tests/test_all.py       | 20 ++++++++++++++-----
 uv.lock                 |  4 ++--
 5 files changed, 79 insertions(+), 20 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 540e8c2..fdcd703 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
 
 [project]
 name            = "apache-trusted-releases"
-version         = "0.20250710.1854"
+version         = "0.20250710.1944"
 description     = "ATR CLI and Python API"
 readme          = "README.md"
 requires-python = ">=3.13"
@@ -48,4 +48,4 @@ atr = "atrclient.client:main"
 packages = ["src/atrclient"]
 
 [tool.uv]
-exclude-newer = "2025-07-10T18:54:00Z"
+exclude-newer = "2025-07-10T19:44:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index f996750..8024eb2 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -182,6 +182,15 @@ def app_dev_env() -> None:
     print(f"There are {total} ATR_* environment variables.")
 
 
+@APP_DEV.command(name="pat", help="Read a PAT from development configuration.")
+def app_dev_pat() -> None:
+    atr_pat_path = pathlib.Path.home() / ".atr-pat"
+    if not atr_pat_path.exists():
+        show_error_and_exit("~/.atr-pat not found.")
+    text = atr_pat_path.read_text(encoding="utf-8").removesuffix("\n")
+    print(text)
+
+
 @APP_DEV.command(
     name="stamp", help="Update version and exclude-newer in pyproject.toml."
 )
@@ -235,6 +244,12 @@ def app_dev_token() -> None:
     print(label)
 
 
+@APP_DEV.command(name="user", help="Show the value of $USER.")
+def app_dev_user() -> None:
+    # This does not help if your ASF UID is not the same as $USER
+    print(os.environ["USER"])
+
+
 @APP_DRAFT.command(name="delete", help="Delete a draft release.")
 def app_draft_delete(project: str, version: str, /) -> None:
     jwt_value = config_jwt_usable()
@@ -271,6 +286,8 @@ def app_drop(path: str, /) -> None:
 @APP_JWT.command(name="dump", help="Show decoded JWT payload from stored 
config.")
 def app_jwt_dump() -> None:
     jwt_value = config_jwt_get()
+    if jwt_value is None:
+        show_error_and_exit("No JWT stored in configuration.")
 
     header = jwt.get_unverified_header(jwt_value)
     if header != {"alg": "HS256", "typ": "JWT"}:
@@ -286,7 +303,9 @@ def app_jwt_dump() -> None:
 
 @APP_JWT.command(name="info", help="Show JWT payload in human-readable form.")
 def app_jwt_info() -> None:
-    _jwt_value, payload = config_jwt_payload()
+    jwt_value, payload = config_jwt_payload()
+    if jwt_value is None:
+        show_error_and_exit("No JWT stored in configuration.")
 
     lines: list[str] = []
     for key, val in payload.items():
@@ -536,18 +555,16 @@ def config_host_get() -> tuple[str, bool]:
     return host, verify_ssl
 
 
-def config_jwt_get() -> str:
+def config_jwt_get() -> str | None:
     with config_lock() as config:
         jwt_value = config_get(config, ["tokens", "jwt"])
-
-    if jwt_value is None:
-        show_error_and_exit("No JWT stored in configuration.")
-
     return jwt_value
 
 
-def config_jwt_payload() -> tuple[str, dict[str, Any]]:
+def config_jwt_payload() -> tuple[str | None, dict[str, Any]]:
     jwt_value = config_jwt_get()
+    if jwt_value is None:
+        return None, {}
     if jwt_value == "dummy_jwt_token":
         # TODO: Use a better test JWT
         return jwt_value, {"exp": time.time() + 90 * 60, "sub": "test_asf_uid"}
@@ -587,9 +604,19 @@ def config_jwt_refresh(asf_uid: str | None = None) -> str:
 
 def config_jwt_usable() -> str:
     jwt_value, payload = config_jwt_payload()
-    if (payload.get("exp") or 0) < time.time():
+    if jwt_value is None:
+        with config_lock() as config:
+            asf_uid = config_get(config, ["asf", "uid"])
+        if asf_uid is None:
+            show_error_and_exit("No ASF UID stored in configuration.")
+        return config_jwt_refresh(asf_uid)
+
+    exp = payload.get("exp") or 0
+    if exp < time.time():
         asf_uid = payload.get("sub")
-        jwt_value = config_jwt_refresh(asf_uid)
+        if not asf_uid:
+            show_error_and_exit("No ASF UID in JWT payload.")
+        return config_jwt_refresh(asf_uid)
     return jwt_value
 
 
@@ -810,14 +837,16 @@ async def web_fetch(
     url: str, asfuid: str, pat_token: str, verify_ssl: bool = True
 ) -> str:
     # TODO: This is PAT request specific
-    # Should give this a more specific name
+    # Should give this a more specific name, e.g. web_post_pat
     connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
     async with aiohttp.ClientSession(connector=connector) as session:
         payload = {"asfuid": asfuid, "pat": pat_token}
         async with session.post(url, json=payload) as resp:
             if resp.status != 200:
                 text = await resp.text()
-                show_error_and_exit(f"JWT fetch failed: {resp.status} {text}")
+                show_error_and_exit(
+                    f"JWT fetch failed: {payload!r} {resp.status} {text!r}"
+                )
 
             data: dict[str, Any] = await resp.json()
             if "jwt" in data:
diff --git a/tests/cli_workflow.t b/tests/cli_workflow.t
new file mode 100644
index 0000000..48cc61c
--- /dev/null
+++ b/tests/cli_workflow.t
@@ -0,0 +1,20 @@
+$ atr set atr.host 127.0.0.1:8080
+Set atr.host to "127.0.0.1:8080".
+
+$ atr dev user
+<?user?>
+
+$ atr set asf.uid <!user!>
+Set asf.uid to "<!user!>".
+
+$ atr dev pat
+<?pat?>
+
+$ atr set tokens.pat <!pat!>
+Set tokens.pat to "<!pat!>".
+
+* atr draft delete tooling-test-example 0.3+cli
+<.etc.>
+
+$ atr release start tooling-test-example 0.3+cli
+<.skip.>created<.skip.>
diff --git a/tests/test_all.py b/tests/test_all.py
index 57c0607..50523bc 100755
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -221,17 +221,27 @@ def test_cli_transcripts(
             line = line.rstrip("\n")
             if captures:
                 line = substitute_uses(captures, line)
-            if line.startswith("$ ") or line.startswith("! "):
-                expected_code = 0 if line.startswith("$ ") else 1
+            if line.startswith("$ ") or line.startswith("! ") or 
line.startswith("* "):
+                match line[:1]:
+                    case "$":
+                        expected_code = 0
+                    case "!":
+                        expected_code = 1
+                    case "*":
+                        expected_code = None
+                    case _:
+                        pytest.fail(f"Unknown line prefix: {line[:1]!r}")
+                        return
                 command = line[2:]
                 if not command.startswith("atr"):
                     pytest.fail(f"Command does not start with 'atr': 
{command}")
                     return
                 print(f"Running: {command}")
                 result = script_runner.run(shlex.split(command), env=env)
-                assert result.returncode == expected_code, (
-                    f"Command {command!r} returned {result.returncode}"
-                )
+                if expected_code is not None:
+                    assert result.returncode == expected_code, (
+                        f"Command {command!r} returned {result.returncode}"
+                    )
                 actual_output[:] = result.stdout.splitlines()
                 if result.stderr:
                     actual_output.append("<.stderr.>")
diff --git a/uv.lock b/uv.lock
index 439b332..3723fd9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
 requires-python = ">=3.13"
 
 [options]
-exclude-newer = "2025-07-10T18:54:00Z"
+exclude-newer = "2025-07-10T19:44:00Z"
 
 [[package]]
 name = "aiohappyeyeballs"
@@ -74,7 +74,7 @@ wheels = [
 
 [[package]]
 name = "apache-trusted-releases"
-version = "0.20250710.1854"
+version = "0.20250710.1944"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to