cloudsmith-cli 1.23.0__tar.gz → 1.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {cloudsmith_cli-1.23.0/cloudsmith_cli.egg-info → cloudsmith_cli-1.24.0}/PKG-INFO +17 -3
  2. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/README.md +15 -1
  3. cloudsmith_cli-1.24.0/cloudsmith_cli/__main__.py +15 -0
  4. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/auth.py +41 -4
  5. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/push.py +77 -0
  6. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/decorators.py +24 -1
  7. cloudsmith_cli-1.24.0/cloudsmith_cli/cli/dsc_parser.py +268 -0
  8. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/exceptions.py +3 -2
  9. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/aws.py +33 -1
  10. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/keyring.py +31 -0
  11. cloudsmith_cli-1.24.0/cloudsmith_cli/data/VERSION +1 -0
  12. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0/cloudsmith_cli.egg-info}/PKG-INFO +17 -3
  13. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli.egg-info/SOURCES.txt +1 -0
  14. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli.egg-info/requires.txt +1 -1
  15. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/pyproject.toml +2 -1
  16. cloudsmith_cli-1.23.0/cloudsmith_cli/__main__.py +0 -8
  17. cloudsmith_cli-1.23.0/cloudsmith_cli/data/VERSION +0 -1
  18. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/LICENSE +0 -0
  19. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/MANIFEST.in +0 -0
  20. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/__init__.py +0 -0
  21. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/__init__.py +0 -0
  22. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/command.py +0 -0
  23. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/__init__.py +0 -0
  24. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/check.py +0 -0
  25. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
  26. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/credential_helper/__init__.py +0 -0
  27. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/credential_helper/docker.py +0 -0
  28. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/credential_helper/generic.py +0 -0
  29. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/credential_helper/manage.py +0 -0
  30. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
  31. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
  32. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
  33. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/domains.py +0 -0
  34. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/download.py +0 -0
  35. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/entitlements.py +0 -0
  36. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
  37. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
  38. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/login.py +0 -0
  39. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/logout.py +0 -0
  40. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/main.py +0 -0
  41. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/mcp.py +0 -0
  42. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/metadata.py +0 -0
  43. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
  44. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
  45. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
  46. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
  47. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/move.py +0 -0
  48. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
  49. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
  50. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
  51. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
  52. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
  53. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
  54. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
  55. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
  56. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
  57. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
  58. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
  59. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
  60. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/status.py +0 -0
  61. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
  62. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/tokens.py +0 -0
  63. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/upstream.py +0 -0
  64. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/vulnerabilities.py +0 -0
  65. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/commands/whoami.py +0 -0
  66. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/config.py +0 -0
  67. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/metadata_common.py +0 -0
  68. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/saml.py +0 -0
  69. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/table.py +0 -0
  70. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/types.py +0 -0
  71. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/utils.py +0 -0
  72. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/validators.py +0 -0
  73. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/cli/webserver.py +0 -0
  74. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/conftest.py +0 -0
  75. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/__init__.py +0 -0
  76. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/__init__.py +0 -0
  77. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/distros.py +0 -0
  78. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
  79. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
  80. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/files.py +0 -0
  81. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/init.py +0 -0
  82. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/metadata.py +0 -0
  83. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/metrics.py +0 -0
  84. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/orgs.py +0 -0
  85. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/packages.py +0 -0
  86. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/quota.py +0 -0
  87. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/rates.py +0 -0
  88. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/repos.py +0 -0
  89. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/status.py +0 -0
  90. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
  91. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/user.py +0 -0
  92. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/version.py +0 -0
  93. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/api/vulnerabilities.py +0 -0
  94. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/cache_utils.py +0 -0
  95. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/config.py +0 -0
  96. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/__init__.py +0 -0
  97. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/chain.py +0 -0
  98. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/models.py +0 -0
  99. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/__init__.py +0 -0
  100. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/cache.py +0 -0
  101. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +0 -0
  102. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +0 -0
  103. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/base.py +0 -0
  104. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +0 -0
  105. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +0 -0
  106. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/generic.py +0 -0
  107. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +0 -0
  108. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +0 -0
  109. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/oidc/exchange.py +0 -0
  110. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/provider.py +0 -0
  111. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/providers/__init__.py +0 -0
  112. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/providers/cli_flag.py +0 -0
  113. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/providers/credentials_file.py +0 -0
  114. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/providers/env_var.py +0 -0
  115. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/providers/keyring_provider.py +0 -0
  116. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/credentials/providers/oidc_provider.py +0 -0
  117. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/download.py +0 -0
  118. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
  119. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/mcp/data.py +0 -0
  120. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/mcp/server.py +0 -0
  121. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/pagination.py +0 -0
  122. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/ratelimits.py +0 -0
  123. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/rest.py +0 -0
  124. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/utils.py +0 -0
  125. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/core/version.py +0 -0
  126. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/__init__.py +0 -0
  127. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/backends.py +0 -0
  128. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/common.py +0 -0
  129. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/custom_domains.py +0 -0
  130. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/default_domains.py +0 -0
  131. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/docker/__init__.py +0 -0
  132. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/docker/installer.py +0 -0
  133. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/docker/runtime.py +0 -0
  134. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/generic.py +0 -0
  135. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/credential_helpers/launchers.py +0 -0
  136. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/data/config.ini +0 -0
  137. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/data/credentials.ini +0 -0
  138. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/templates/__init__.py +0 -0
  139. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/templates/auth_error.html +0 -0
  140. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli/templates/auth_success.html +0 -0
  141. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
  142. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
  143. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
  144. {cloudsmith_cli-1.23.0 → cloudsmith_cli-1.24.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudsmith-cli
3
- Version: 1.23.0
3
+ Version: 1.24.0
4
4
  Summary: Cloudsmith Command-Line Interface (CLI)
5
5
  Author-email: Cloudsmith Ltd <support@cloudsmith.io>
6
6
  License-Expression: Apache-2.0
@@ -23,7 +23,7 @@ Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.10.0
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: click!=8.3.0,>=8.1.8
26
+ Requires-Dist: click!=8.3.0,>=8.2
27
27
  Requires-Dist: click-configfile>=0.2.3
28
28
  Requires-Dist: click-didyoumean>=0.0.3
29
29
  Requires-Dist: click-spinner>=0.1.7
@@ -367,13 +367,21 @@ You can authenticate using your organization's SAML provider, if configured, wit
367
367
  ```
368
368
  cloudsmith auth --owner example
369
369
  Beginning authentication for the example org ...
370
- Opening your organization's SAML IDP URL in your browser: https://example.com/some-saml-idp
370
+ Your organization's SAML IDP URL is: https://example.com/some-saml-idp
371
371
 
372
372
  Starting webserver to begin authentication ...
373
373
 
374
374
  Authentication complete
375
375
  ```
376
376
 
377
+ The CLI opens the IDP URL in your browser automatically. If it can't (for example in Cygwin or a headless shell), it tells you to open the URL above manually and carries on waiting for the callback. Use `--no-browser` to skip the automatic launch entirely:
378
+
379
+ ```
380
+ cloudsmith auth --owner example --no-browser
381
+ ```
382
+
383
+ *Note:* The authentication callback is served on `127.0.0.1:12400`, so the browser you open the URL in must be running on the same machine as the CLI.
384
+
377
385
  #### Getting Your API Key
378
386
 
379
387
  You can retrieve your API key using the `cloudsmith login` command:
@@ -411,6 +419,12 @@ For example, if you wanted to upload a Debian package, you can do it in one-step
411
419
  cloudsmith push deb your-org/your-repo/ubuntu/xenial libxml2-2.9.4-2.x86_64.deb
412
420
  ```
413
421
 
422
+ To upload a Debian *source* package, give it the `.dsc`. The source and Debian packaging archives listed in the `.dsc` are found and uploaded with it:
423
+
424
+ ```
425
+ cloudsmith push deb your-org/your-repo/ubuntu/xenial hello_2.10-3.dsc
426
+ ```
427
+
414
428
  Want to know how to do it with another packaging format? Easy, just ask for help:
415
429
 
416
430
  ```
@@ -319,13 +319,21 @@ You can authenticate using your organization's SAML provider, if configured, wit
319
319
  ```
320
320
  cloudsmith auth --owner example
321
321
  Beginning authentication for the example org ...
322
- Opening your organization's SAML IDP URL in your browser: https://example.com/some-saml-idp
322
+ Your organization's SAML IDP URL is: https://example.com/some-saml-idp
323
323
 
324
324
  Starting webserver to begin authentication ...
325
325
 
326
326
  Authentication complete
327
327
  ```
328
328
 
329
+ The CLI opens the IDP URL in your browser automatically. If it can't (for example in Cygwin or a headless shell), it tells you to open the URL above manually and carries on waiting for the callback. Use `--no-browser` to skip the automatic launch entirely:
330
+
331
+ ```
332
+ cloudsmith auth --owner example --no-browser
333
+ ```
334
+
335
+ *Note:* The authentication callback is served on `127.0.0.1:12400`, so the browser you open the URL in must be running on the same machine as the CLI.
336
+
329
337
  #### Getting Your API Key
330
338
 
331
339
  You can retrieve your API key using the `cloudsmith login` command:
@@ -363,6 +371,12 @@ For example, if you wanted to upload a Debian package, you can do it in one-step
363
371
  cloudsmith push deb your-org/your-repo/ubuntu/xenial libxml2-2.9.4-2.x86_64.deb
364
372
  ```
365
373
 
374
+ To upload a Debian *source* package, give it the `.dsc`. The source and Debian packaging archives listed in the `.dsc` are found and uploaded with it:
375
+
376
+ ```
377
+ cloudsmith push deb your-org/your-repo/ubuntu/xenial hello_2.10-3.dsc
378
+ ```
379
+
366
380
  Want to know how to do it with another packaging format? Easy, just ask for help:
367
381
 
368
382
  ```
@@ -0,0 +1,15 @@
1
+ """Cloudsmith CLI - Main script."""
2
+
3
+ import sys
4
+
5
+ from .cli.commands.main import main
6
+
7
+ if __name__ == "__main__":
8
+ # sys.exit() is required: AliasGroup.main runs click with
9
+ # standalone_mode=False, so click returns the exit code (e.g. from
10
+ # ctx.exit()) instead of raising SystemExit. The console script and the
11
+ # PyInstaller entry point wrap main() in sys.exit() too; a bare main()
12
+ # call would discard the code and always exit 0.
13
+ # Disable false positive for parameters handled by click.
14
+ # pylint: disable=no-value-for-parameter
15
+ sys.exit(main())
@@ -17,7 +17,7 @@ AUTH_SERVER_PORT = 12400
17
17
 
18
18
 
19
19
  def _perform_saml_authentication(
20
- opts, owner, enable_token_creation=False, use_stderr=False
20
+ opts, owner, enable_token_creation=False, use_stderr=False, no_browser=False
21
21
  ):
22
22
  """Perform SAML authentication via web browser and local web server."""
23
23
  session = create_configured_session(opts)
@@ -26,11 +26,32 @@ def _perform_saml_authentication(
26
26
  idp_url = get_idp_url(api_host, owner, session=session)
27
27
 
28
28
  click.echo(
29
- f"Opening your organization's SAML IDP URL in your browser: {click.style(idp_url, bold=True)}",
29
+ f"Your organization's SAML IDP URL is: {click.style(idp_url, bold=True)}",
30
30
  err=use_stderr,
31
31
  )
32
32
  click.echo(err=use_stderr)
33
- webbrowser.open(idp_url)
33
+
34
+ if no_browser:
35
+ click.echo(
36
+ "Skipping automatic browser launch (--no-browser). "
37
+ "Please open the URL above manually to continue.",
38
+ err=use_stderr,
39
+ )
40
+ else:
41
+ try:
42
+ browser_opened = webbrowser.open(idp_url)
43
+ except Exception:
44
+ # Browser launch failures vary by platform (webbrowser.Error on
45
+ # Cygwin/headless, anything else elsewhere), so catch broadly and
46
+ # fall back to the manual URL rather than crashing.
47
+ browser_opened = False
48
+
49
+ if not browser_opened:
50
+ click.echo(
51
+ "Couldn't open a browser automatically. "
52
+ "Please open the URL above manually to continue.",
53
+ err=use_stderr,
54
+ )
34
55
 
35
56
  click.echo("Starting webserver to begin authentication ... ", err=use_stderr)
36
57
 
@@ -91,12 +112,27 @@ def _perform_saml_authentication(
91
112
  help="Retrieve API token (auto-creates or auto-rotates, no prompts). "
92
113
  "Warning: If token exists, this will rotate it and invalidate the old key.",
93
114
  )
115
+ @click.option(
116
+ "--no-browser",
117
+ default=False,
118
+ is_flag=True,
119
+ help="Don't try to open a browser automatically; print the SAML IDP URL to "
120
+ "open manually instead.",
121
+ )
94
122
  @decorators.common_cli_config_options
95
123
  @decorators.common_cli_output_options
96
124
  @decorators.initialise_api
97
125
  @click.pass_context
98
126
  def authenticate(
99
- ctx, opts, owner, token, force, save_config, json, request_api_key_flag
127
+ ctx,
128
+ opts,
129
+ owner,
130
+ token,
131
+ force,
132
+ save_config,
133
+ json,
134
+ request_api_key_flag,
135
+ no_browser,
100
136
  ):
101
137
  """Authenticate to Cloudsmith using the org's SAML setup."""
102
138
  # Validate mutual exclusivity
@@ -150,6 +186,7 @@ def authenticate(
150
186
  owner,
151
187
  enable_token_creation=enable_token_creation,
152
188
  use_stderr=use_stderr,
189
+ no_browser=no_browser,
153
190
  )
154
191
 
155
192
  if request_api_key_flag:
@@ -38,6 +38,7 @@ from ...core.api.packages import (
38
38
  validate_create_package as api_validate_create_package,
39
39
  )
40
40
  from .. import command, decorators, utils, validators
41
+ from ..dsc_parser import resolve_dsc_files
41
42
  from ..exceptions import handle_api_exceptions
42
43
  from ..metadata_common import (
43
44
  MetadataContentError,
@@ -75,6 +76,10 @@ METADATA_KWARG_NAMES = (
75
76
  #: separately from the metadata payload kwargs so it does not leak into the
76
77
  #: package-create API call.
77
78
  METADATA_FAILURE_MODE_KWARG = "cli_metadata_failure_mode"
79
+ #: Filename suffix of a Debian source control file. A ``deb`` push whose
80
+ #: PACKAGE_FILE carries it is a source-package upload, so its members can be
81
+ #: derived without ``--dsc-file`` (GitHub issue #56).
82
+ DSC_SUFFIX = ".dsc"
78
83
 
79
84
 
80
85
  def _metadata_failure_is_warn(opts=None):
@@ -1019,6 +1024,13 @@ def upload_files_and_create_package(
1019
1024
  return slug_perm, slug
1020
1025
 
1021
1026
 
1027
+ def _implied_dsc_file(package_file):
1028
+ """Return ``package_file`` if it is itself a Debian ``.dsc``, else None."""
1029
+ if isinstance(package_file, str) and package_file.endswith(DSC_SUFFIX):
1030
+ return package_file
1031
+ return None
1032
+
1033
+
1022
1034
  def create_push_handlers():
1023
1035
  """Create a handler for upload per package format."""
1024
1036
  # pylint: disable=fixme
@@ -1181,6 +1193,38 @@ def create_push_handlers():
1181
1193
  parameters = context.get(ctx.info_name)
1182
1194
  kwargs["package_type"] = ctx.info_name
1183
1195
 
1196
+ # deb-only: derive the upstream/native source archive and Debian
1197
+ # packaging archive from a .dsc (see GitHub issue #56). Only the
1198
+ # `deb` subcommand ever registers --dsc-file (below), so this is
1199
+ # a no-op kwargs.pop() for every other format.
1200
+ dsc_file = kwargs.pop("dsc_file", None)
1201
+ if dsc_file is None and not kwargs.get("sources_file"):
1202
+ # Pushing a Debian source package means passing the .dsc as
1203
+ # PACKAGE_FILE, and the API rejects it without a sources
1204
+ # archive, so parse it by default rather than making the user
1205
+ # name the same file twice. An explicit --sources-file means
1206
+ # the caller is driving the members manually; leave them to it.
1207
+ dsc_file = _implied_dsc_file(kwargs.get("package_file"))
1208
+ if dsc_file:
1209
+ resolved_dsc = resolve_dsc_files(dsc_file)
1210
+ if resolved_dsc.ignored_files:
1211
+ click.secho(
1212
+ "Not uploading {files}: the deb package format has no "
1213
+ "field for detached upstream signatures.".format(
1214
+ files=", ".join(resolved_dsc.ignored_files)
1215
+ ),
1216
+ fg="yellow",
1217
+ err=utils.should_use_stderr(opts),
1218
+ )
1219
+ # Precedence: explicit --sources-file/--changes-file always
1220
+ # win over values derived from --dsc-file. A user who passes
1221
+ # both wants a manual override, not to have their explicit
1222
+ # flag silently clobbered.
1223
+ if not kwargs.get("sources_file"):
1224
+ kwargs["sources_file"] = resolved_dsc.sources_file
1225
+ if not kwargs.get("changes_file"):
1226
+ kwargs["changes_file"] = resolved_dsc.changes_file
1227
+
1184
1228
  owner_repo = kwargs.pop("owner_repo")
1185
1229
  if "distribution" in parameters:
1186
1230
  kwargs["distribution"] = "/".join(owner_repo[2:])
@@ -1307,6 +1351,39 @@ def create_push_handlers():
1307
1351
  )
1308
1352
  push_handler = decorator(push_handler)
1309
1353
 
1354
+ if key == "deb":
1355
+ # deb-only shim (GitHub issue #56): the generic per-format loop
1356
+ # above is driven entirely by the API's package-format model, so
1357
+ # this stays layered on top rather than becoming a new branch in
1358
+ # that generic system.
1359
+ push_handler = click.option(
1360
+ "--dsc-file",
1361
+ "dsc_file",
1362
+ type=ExpandPath(
1363
+ dir_okay=False, exists=True, writable=False, resolve_path=True
1364
+ ),
1365
+ default=None,
1366
+ help=(
1367
+ "Path to a Debian .dsc control file, plain or "
1368
+ "OpenPGP-clearsigned. Only needed to point at a .dsc "
1369
+ "other than PACKAGE_FILE: pushing a source package means "
1370
+ "passing the .dsc as PACKAGE_FILE, and that is parsed "
1371
+ "automatically unless --sources-file is given. Its "
1372
+ "'Checksums-Sha256:' (or 'Files:') field supplies "
1373
+ "--sources-file with the upstream/native source archive "
1374
+ "and --changes-file with the Debian packaging archive "
1375
+ "(.debian.tar.* or .diff.gz). Supports the 1.0, 2.0, "
1376
+ "3.0 (native) and 3.0 (quilt) source formats, and the "
1377
+ "referenced files must sit next to the .dsc. An explicit "
1378
+ "--sources-file or --changes-file always takes precedence "
1379
+ "over the derived value for that field. Detached upstream "
1380
+ "signatures (*.asc) are skipped with a warning; a "
1381
+ "multi-component source package (*.orig-*.tar.*) is "
1382
+ "rejected, because the deb package format has no field "
1383
+ "for the extra components."
1384
+ ),
1385
+ )(push_handler)
1386
+
1310
1387
  handlers[key] = push_handler
1311
1388
 
1312
1389
 
@@ -1,6 +1,7 @@
1
1
  """CLI - Decorators."""
2
2
 
3
3
  import functools
4
+ import logging
4
5
  import os
5
6
 
6
7
  import click
@@ -28,6 +29,27 @@ def report_retry(seconds, context=None):
28
29
  )
29
30
 
30
31
 
32
+ _DEBUG_LOG_HANDLER_NAME = "cloudsmith-cli-debug"
33
+
34
+
35
+ def _configure_debug_logging(enabled):
36
+ """Install or remove the stderr handler for this package's debug records."""
37
+ package_logger = logging.getLogger("cloudsmith_cli")
38
+ for handler in list(package_logger.handlers):
39
+ if handler.name == _DEBUG_LOG_HANDLER_NAME:
40
+ package_logger.removeHandler(handler)
41
+ if not enabled:
42
+ package_logger.setLevel(logging.NOTSET)
43
+ package_logger.propagate = True
44
+ return
45
+ package_logger.setLevel(logging.DEBUG)
46
+ package_logger.propagate = False
47
+ handler = logging.StreamHandler()
48
+ handler.name = _DEBUG_LOG_HANDLER_NAME
49
+ handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
50
+ package_logger.addHandler(handler)
51
+
52
+
31
53
  def _pop_boolean_flag(kwargs, name, invert=False):
32
54
  """Pop a boolean flag from kwargs, optionally inverting it."""
33
55
  value = kwargs.pop(name)
@@ -158,7 +180,8 @@ def common_cli_output_options(f):
158
180
  def wrapper(ctx, *args, **kwargs):
159
181
  # pylint: disable=missing-docstring
160
182
  opts = config.get_or_create_options(ctx)
161
- opts.debug = kwargs.pop("debug")
183
+ opts.debug = kwargs.pop("debug") or opts.debug
184
+ _configure_debug_logging(opts.debug)
162
185
  opts.output = kwargs.pop("output_format")
163
186
  opts.verbose = kwargs.pop("verbose")
164
187
  kwargs["opts"] = opts
@@ -0,0 +1,268 @@
1
+ """Parse Debian ``.dsc`` control files for ``push deb`` source uploads."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from email.parser import Parser
6
+
7
+ import click
8
+
9
+ _CLEAR_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----"
10
+ _SIGNATURE = "-----BEGIN PGP SIGNATURE-----"
11
+ _DETACHED_SIGNATURE_SUFFIX = ".asc"
12
+ _FILE_LIST_FIELDS = ("Checksums-Sha256", "Files")
13
+ _QUILT_FORMATS = {"2.0", "3.0 (quilt)"}
14
+ _NATIVE_FORMAT = "3.0 (native)"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ResolvedDscFiles:
19
+ """Source-package members resolved from a Debian ``.dsc``."""
20
+
21
+ sources_file: str
22
+ changes_file: str | None = None
23
+ #: Members deliberately left out of the upload, for the caller to report.
24
+ ignored_files: tuple[str, ...] = field(default_factory=tuple)
25
+
26
+
27
+ def _usage_error(dsc_path, message):
28
+ # Names the file rather than the option: the .dsc is just as often taken
29
+ # from PACKAGE_FILE as from an explicit --dsc-file.
30
+ return click.UsageError(f"Debian source control file {dsc_path!r} {message}")
31
+
32
+
33
+ def _unwrap_clearsigned_control(text, dsc_path):
34
+ """Return deb822 control text from an optional OpenPGP cleartext signature."""
35
+ lines = text.splitlines(keepends=True)
36
+ if not lines or lines[0].rstrip("\r\n") != _CLEAR_SIGNED_MESSAGE:
37
+ return text
38
+
39
+ index = 1
40
+ while index < len(lines) and lines[index].rstrip("\r\n"):
41
+ index += 1
42
+ if index == len(lines):
43
+ raise _usage_error(dsc_path, "has a malformed OpenPGP cleartext signature.")
44
+
45
+ cleartext = []
46
+ for line in lines[index + 1 :]:
47
+ if line.rstrip("\r\n") == _SIGNATURE:
48
+ return "".join(cleartext)
49
+ # RFC 9580 cleartext signatures dash-escape lines beginning with "-".
50
+ cleartext.append(line.removeprefix("- "))
51
+
52
+ raise _usage_error(dsc_path, "has a malformed OpenPGP cleartext signature.")
53
+
54
+
55
+ def _read_control_message(dsc_path):
56
+ """Parse ``dsc_path`` as a plain or OpenPGP-clearsigned deb822 stanza."""
57
+ try:
58
+ with open(dsc_path, encoding="utf-8", errors="replace") as dsc_fh:
59
+ text = dsc_fh.read()
60
+ except OSError as exc:
61
+ raise _usage_error(dsc_path, f"could not be read: {exc}") from exc
62
+
63
+ return Parser().parsestr(_unwrap_clearsigned_control(text, dsc_path))
64
+
65
+
66
+ def _parse_file_list(field_name, field_value, dsc_path):
67
+ filenames = []
68
+ for line in field_value.splitlines():
69
+ if not line.strip():
70
+ continue
71
+ parts = line.split()
72
+ if len(parts) != 3:
73
+ raise _usage_error(
74
+ dsc_path,
75
+ f"has a malformed {field_name!r} entry {line.strip()!r}; "
76
+ "expected '<checksum> <size> <filename>'.",
77
+ )
78
+ filenames.append(parts[2])
79
+ return filenames
80
+
81
+
82
+ def _extract_filenames(message, dsc_path):
83
+ """Return an agreed filename list, preferring the strong SHA-256 field."""
84
+ file_lists = {}
85
+ for field_name in _FILE_LIST_FIELDS:
86
+ field_value = message.get(field_name)
87
+ if field_value:
88
+ filenames = _parse_file_list(field_name, field_value, dsc_path)
89
+ if filenames:
90
+ file_lists[field_name] = filenames
91
+
92
+ if not file_lists:
93
+ raise _usage_error(
94
+ dsc_path,
95
+ "has no non-empty 'Checksums-Sha256:' or 'Files:' field to parse.",
96
+ )
97
+
98
+ for filenames in file_lists.values():
99
+ if len(filenames) != len(set(filenames)):
100
+ raise _usage_error(dsc_path, "lists the same source-package member twice.")
101
+
102
+ selected_name = next(iter(file_lists))
103
+ selected = file_lists[selected_name]
104
+ for field_name, filenames in file_lists.items():
105
+ if set(filenames) != set(selected):
106
+ raise _usage_error(
107
+ dsc_path,
108
+ f"has conflicting filenames in {selected_name!r} and {field_name!r}.",
109
+ )
110
+
111
+ return selected
112
+
113
+
114
+ def _required_field(message, field_name, dsc_path):
115
+ value = message.get(field_name)
116
+ if not value or not value.strip():
117
+ raise _usage_error(dsc_path, f"has no {field_name!r} field.")
118
+ return value.strip()
119
+
120
+
121
+ def _validate_member_names(filenames, dsc_path):
122
+ for filename in filenames:
123
+ if os.path.isabs(filename) or os.path.basename(filename) != filename:
124
+ raise _usage_error(
125
+ dsc_path,
126
+ f"references invalid member filename {filename!r}; source-package "
127
+ "members must be filenames next to the .dsc, not paths.",
128
+ )
129
+
130
+
131
+ def _is_tar_archive(filename, stem):
132
+ prefix = f"{stem}.tar."
133
+ return filename.startswith(prefix) and len(filename) > len(prefix)
134
+
135
+
136
+ def _classify_members(message, filenames, dsc_path):
137
+ """Map source-package members to the two fields accepted by the SDK."""
138
+ source = _required_field(message, "Source", dsc_path)
139
+ version = _required_field(message, "Version", dsc_path).split(":", 1)[-1]
140
+ source_format = _required_field(message, "Format", dsc_path)
141
+ upstream_version = version.rsplit("-", 1)[0]
142
+
143
+ # Detached upstream signatures (e.g. hello_2.10.orig.tar.gz.asc) are
144
+ # common and have no field in the deb upload model. Nothing in the
145
+ # uploaded source is lost by leaving them out, so they are skipped with a
146
+ # warning rather than failing the push.
147
+ signature_files = [
148
+ filename
149
+ for filename in filenames
150
+ if filename.endswith(_DETACHED_SIGNATURE_SUFFIX)
151
+ ]
152
+ # Classify the uploadable members only. A signature shares its tarball's
153
+ # stem (`*.orig.tar.gz.asc`), so leaving it in would match as a second
154
+ # source archive.
155
+ members = [f for f in filenames if f not in set(signature_files)]
156
+
157
+ sources = []
158
+ changes = []
159
+ component_files = []
160
+ legacy_non_native = False
161
+
162
+ if source_format in _QUILT_FORMATS:
163
+ orig_stem = f"{source}_{upstream_version}.orig"
164
+ component_prefix = f"{orig_stem}-"
165
+ debian_stem = f"{source}_{version}.debian"
166
+ sources = [f for f in members if _is_tar_archive(f, orig_stem)]
167
+ changes = [f for f in members if _is_tar_archive(f, debian_stem)]
168
+ component_files = [
169
+ f
170
+ for f in members
171
+ if f.startswith(component_prefix) and ".tar." in f[len(component_prefix) :]
172
+ ]
173
+ elif source_format == "1.0":
174
+ orig_stem = f"{source}_{upstream_version}.orig"
175
+ diff_name = f"{source}_{version}.diff.gz"
176
+ native_stem = f"{source}_{version}"
177
+ orig_files = [f for f in members if _is_tar_archive(f, orig_stem)]
178
+ diff_files = [f for f in members if f == diff_name]
179
+ native_files = [f for f in members if _is_tar_archive(f, native_stem)]
180
+ if orig_files or diff_files:
181
+ legacy_non_native = True
182
+ sources = orig_files
183
+ changes = diff_files
184
+ else:
185
+ sources = native_files
186
+ elif source_format == _NATIVE_FORMAT:
187
+ sources = [f for f in members if _is_tar_archive(f, f"{source}_{version}")]
188
+ else:
189
+ # '3.0 (git)' ships a git bundle and '3.0 (bzr)' a VCS tarball, neither
190
+ # of which is a source archive the deb upload model can index, so both
191
+ # fall through to the unsupported-format error alongside
192
+ # '3.0 (custom)'.
193
+ raise _usage_error(
194
+ dsc_path, f"uses unsupported Debian source format {source_format!r}."
195
+ )
196
+
197
+ if component_files:
198
+ raise _usage_error(
199
+ dsc_path,
200
+ "references a multi-component source package ({files}). Cloudsmith "
201
+ "does not support multi-component Debian source packages for deb "
202
+ "uploads.".format(files=", ".join(component_files)),
203
+ )
204
+
205
+ classified = set(sources + changes + component_files)
206
+ unexpected = [f for f in members if f not in classified]
207
+ if unexpected:
208
+ raise _usage_error(
209
+ dsc_path,
210
+ "contains unsupported or incorrectly named source-package member(s) "
211
+ f"({', '.join(unexpected)}) for format {source_format!r}.",
212
+ )
213
+ if len(sources) != 1:
214
+ raise _usage_error(
215
+ dsc_path,
216
+ f"must reference exactly one main source archive for format "
217
+ f"{source_format!r}; found {len(sources)}.",
218
+ )
219
+ if source_format in _QUILT_FORMATS and len(changes) != 1:
220
+ raise _usage_error(
221
+ dsc_path,
222
+ f"must reference exactly one Debian packaging archive for format "
223
+ f"{source_format!r}; found {len(changes)}.",
224
+ )
225
+ if legacy_non_native and len(changes) != 1:
226
+ raise _usage_error(
227
+ dsc_path,
228
+ "must reference exactly one Debian packaging diff for non-native "
229
+ f"format '1.0'; found {len(changes)}.",
230
+ )
231
+
232
+ return sources[0], changes[0] if changes else None, tuple(signature_files)
233
+
234
+
235
+ def _resolve_member(base_dir, filename, dsc_path):
236
+ """Canonicalise a member that ``_validate_member_names`` has vetted."""
237
+ candidate = os.path.join(base_dir, filename)
238
+ if not os.path.isfile(candidate):
239
+ raise _usage_error(
240
+ dsc_path,
241
+ f"references {filename!r}, but it is not a regular file next to the .dsc.",
242
+ )
243
+ # Uploading the canonical path means a symlink swapped between this check
244
+ # and the upload cannot redirect the read. The link itself may point
245
+ # outside the directory: `mk-origtargz --symlink` (the uscan default)
246
+ # routinely symlinks the .orig tarball in from a download cache.
247
+ return os.path.realpath(candidate)
248
+
249
+
250
+ def resolve_dsc_files(dsc_path):
251
+ """Return the :class:`ResolvedDscFiles` derived from a Debian ``.dsc``."""
252
+ message = _read_control_message(dsc_path)
253
+ filenames = _extract_filenames(message, dsc_path)
254
+ _validate_member_names(filenames, dsc_path)
255
+ source_filename, changes_filename, ignored_files = _classify_members(
256
+ message, filenames, dsc_path
257
+ )
258
+
259
+ base_dir = os.path.realpath(os.path.dirname(os.path.abspath(dsc_path)))
260
+ return ResolvedDscFiles(
261
+ sources_file=_resolve_member(base_dir, source_filename, dsc_path),
262
+ changes_file=(
263
+ _resolve_member(base_dir, changes_filename, dsc_path)
264
+ if changes_filename
265
+ else None
266
+ ),
267
+ ignored_files=ignored_files,
268
+ )
@@ -171,8 +171,9 @@ def get_401_error_hint(ctx, opts, exc):
171
171
 
172
172
  if credential:
173
173
  return (
174
- "Since you have an API key set, this probably means "
175
- "you don't have the permission to perform this action."
174
+ "This usually means your API key is invalid, expired, or "
175
+ "lacks access to this resource - check your credentials and "
176
+ "try again."
176
177
  )
177
178
 
178
179
  if ctx.info_name == "token":
@@ -20,6 +20,38 @@ logger = logging.getLogger(__name__)
20
20
  DEFAULT_AUDIENCE = "cloudsmith"
21
21
 
22
22
 
23
+ def _resolve_region(session) -> str | None:
24
+ """Resolve the AWS region with the AWS CLI precedence.
25
+
26
+ The order is: explicit session config, AWS_REGION, AWS_DEFAULT_REGION,
27
+ the config file region, then EC2 instance metadata.
28
+ """
29
+ try:
30
+ # Lazy imports: botocore is an optional dependency (the aws extra).
31
+ from botocore.configprovider import ConfigChainFactory
32
+ from botocore.exceptions import BotoCoreError
33
+ from botocore.utils import BadIMDSRequestError, IMDSRegionProvider
34
+ except ImportError:
35
+ logger.debug("AWSDetector: region providers unavailable", exc_info=True)
36
+ return None
37
+
38
+ # No public accessor exists for the underlying botocore session.
39
+ botocore_session = session._session # pylint: disable=protected-access
40
+ chain = ConfigChainFactory(session=botocore_session).create_config_chain(
41
+ instance_name="region",
42
+ env_var_names=["AWS_REGION", "AWS_DEFAULT_REGION"],
43
+ config_property_names="region",
44
+ )
45
+ region = chain.provide()
46
+ if region:
47
+ return region
48
+ try:
49
+ return IMDSRegionProvider(session=botocore_session).provide()
50
+ except (BotoCoreError, BadIMDSRequestError):
51
+ logger.debug("AWSDetector: IMDS region lookup failed", exc_info=True)
52
+ return None
53
+
54
+
23
55
  class AWSDetector(EnvironmentDetector):
24
56
  """Detects AWS environments and obtains a JWT via STS GetWebIdentityToken."""
25
57
 
@@ -71,7 +103,7 @@ class AWSDetector(EnvironmentDetector):
71
103
 
72
104
  audience = self.context.oidc_audience or DEFAULT_AUDIENCE
73
105
  session = self._session or boto3.Session()
74
- sts = session.client("sts")
106
+ sts = session.client("sts", region_name=_resolve_region(session))
75
107
  response = sts.get_web_identity_token(
76
108
  Audience=[audience],
77
109
  SigningAlgorithm="RS256",