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]