cloudx-proxy 0.17.4__tar.gz → 0.17.6__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 (53) hide show
  1. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/claude.yml +1 -1
  2. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/CHANGELOG.md +10 -0
  3. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/PKG-INFO +1 -1
  4. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/_version.py +2 -2
  5. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/setup.py +32 -14
  6. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/PKG-INFO +1 -1
  7. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/SOURCES.txt +1 -0
  8. cloudx_proxy-0.17.6/tests/test_instance_tags.py +126 -0
  9. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/README.md +0 -0
  10. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/README.md +0 -0
  11. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/architecture.md +0 -0
  12. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/commit-conventions.md +0 -0
  13. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/development.md +0 -0
  14. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/project-overview.md +0 -0
  15. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/ssh-config.md +0 -0
  16. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/roadmap/architecture-review.md +0 -0
  17. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/roadmap/documentation-findings.md +0 -0
  18. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/roadmap/improvement-roadmap.md +0 -0
  19. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.clinerules +0 -0
  20. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.envrc +0 -0
  21. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/dependabot.yml +0 -0
  22. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/codeql.yml +0 -0
  23. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/release.yml +0 -0
  24. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/test.yml +0 -0
  25. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.gitignore +0 -0
  26. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.releaserc +0 -0
  27. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/CLAUDE.md +0 -0
  28. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/CONTRIBUTING.md +0 -0
  29. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/LICENSE +0 -0
  30. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/NOTICE +0 -0
  31. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/README.md +0 -0
  32. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/__init__.py +0 -0
  33. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/_op.py +0 -0
  34. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/cli.py +0 -0
  35. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/colors.py +0 -0
  36. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/core.py +0 -0
  37. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
  38. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/entry_points.txt +0 -0
  39. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/requires.txt +0 -0
  40. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/top_level.txt +0 -0
  41. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/pyproject.toml +0 -0
  42. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/setup.cfg +0 -0
  43. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/__init__.py +0 -0
  44. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_backward_compat.py +0 -0
  45. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_cli_options.py +0 -0
  46. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_config_safety.py +0 -0
  47. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_core.py +0 -0
  48. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_non_interactive.py +0 -0
  49. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_runtime_safety.py +0 -0
  50. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_setup.py +0 -0
  51. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_ssh_config.py +0 -0
  52. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_version.py +0 -0
  53. {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/uv.lock +0 -0
@@ -32,7 +32,7 @@ jobs:
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@v1.0.210
35
+ uses: anthropics/claude-code-action@v1.0.217
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
@@ -1,3 +1,13 @@
1
+ ## [0.17.6](https://github.com/easytocloud/cloudX-proxy/compare/v0.17.5...v0.17.6) (2026-09-16)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * resolve hostname/environment from plain Name tag, not just legacy prefix ([8f157a5](https://github.com/easytocloud/cloudX-proxy/commit/8f157a587a24c1cbb587162fc4e11ed8945b0715))
7
+ * use warning styling, not the failure symbol, for env tag conflicts ([7866ddc](https://github.com/easytocloud/cloudX-proxy/commit/7866ddcd35248d1124c66491f5d8ce7b6b489ba3))
8
+
9
+ ## [0.17.5](https://github.com/easytocloud/cloudX-proxy/compare/v0.17.4...v0.17.5) (2026-09-10)
10
+
1
11
  ## [0.17.4](https://github.com/easytocloud/cloudX-proxy/compare/v0.17.3...v0.17.4) (2026-09-06)
2
12
 
3
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudx-proxy
3
- Version: 0.17.4
3
+ Version: 0.17.6
4
4
  Summary: SSH proxy command to connect VSCode with cloudX instance using AWS Systems Manager
5
5
  Author-email: easytocloud <info@easytocloud.com>
6
6
  License: MIT License
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.17.4'
22
- __version_tuple__ = version_tuple = (0, 17, 4)
21
+ __version__ = version = '0.17.6'
22
+ __version_tuple__ = version_tuple = (0, 17, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -135,8 +135,12 @@ class CloudXSetup:
135
135
  """Fetch instance tags and extract environment and hostname.
136
136
 
137
137
  Queries EC2 for the instance tags and extracts:
138
- - Environment by priority: {env} from Name tag > cloudX:environment tag > Environment tag
139
- - Hostname from the 'Name' tag (expects format: cloudX-{env}-{hostname} | {username})
138
+ - Hostname from the 'Name' tag: legacy cloudX-{env}-{hostname} (optionally
139
+ " | {username}") format if present, otherwise Name is used as-is (current
140
+ cloudX-instance.yaml sets Name to the plain hostname with no prefix).
141
+ - Environment from the cloudX:environment/cloudx:environment/Environment tag,
142
+ preferred over {env} parsed from a legacy Name tag. A mismatch between the
143
+ two is logged as a warning; the tag always wins.
140
144
 
141
145
  Args:
142
146
  instance_id: The EC2 instance ID
@@ -173,8 +177,12 @@ class CloudXSetup:
173
177
  instance = response['Reservations'][0]['Instances'][0]
174
178
  tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}
175
179
 
176
- # Extract hostname and env from Name tag
177
- # Format: cloudX-{env}-{hostname} | {username}
180
+ # Hostname comes from the Name tag. Older cloudX-instance.yaml versions
181
+ # prefixed it as cloudX-{env}-{hostname} (optionally " | {username}");
182
+ # current versions set Name to the plain hostname with no prefix. Try the
183
+ # legacy pattern first for backward compatibility, and fall back to using
184
+ # Name as-is - either way we get a hostname straight from the tag, so this
185
+ # is never itself a reason to flag anything.
178
186
  hostname = None
179
187
  env_from_name = None
180
188
  name_tag = tags.get('Name', '')
@@ -184,20 +192,30 @@ class CloudXSetup:
184
192
  if match:
185
193
  env_from_name = match.group(1)
186
194
  hostname = match.group(2)
187
- self.print_status(f"Found hostname from Name tag: {hostname}", True, 2)
188
195
  else:
189
- self.print_status(f"Name tag '{name_tag}' does not match cloudX-{{env}}-{{hostname}} format", None, 2)
190
-
191
- # Determine environment by priority:
192
- # 1. {env} from Name tag
193
- # 2. cloudX:environment or cloudx:environment tag
194
- # 3. Environment tag
195
- environment = (
196
- env_from_name
197
- or tags.get('cloudX:environment')
196
+ hostname = ssh_hostname
197
+ if hostname:
198
+ self.print_status(f"Found hostname from Name tag: {hostname}", True, 2)
199
+
200
+ # Environment: prefer the explicit tag over one parsed from a legacy Name
201
+ # tag, since the tag is the current, authoritative source. Only speak up
202
+ # if the two genuinely disagree - that's a real inconsistency worth a
203
+ # human's attention, not the routine case.
204
+ env_from_tag = (
205
+ tags.get('cloudX:environment')
198
206
  or tags.get('cloudx:environment')
199
207
  or tags.get('Environment')
200
208
  )
209
+ if env_from_name and env_from_tag and env_from_name != env_from_tag:
210
+ self.print_status(
211
+ warning(
212
+ f"Name tag implies environment '{env_from_name}' but the environment tag says "
213
+ f"'{env_from_tag}' - using '{env_from_tag}'"
214
+ ),
215
+ None, 2
216
+ )
217
+
218
+ environment = env_from_tag or env_from_name
201
219
  if environment:
202
220
  self.print_status(f"Found environment: {environment}", True, 2)
203
221
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudx-proxy
3
- Version: 0.17.4
3
+ Version: 0.17.6
4
4
  Summary: SSH proxy command to connect VSCode with cloudX instance using AWS Systems Manager
5
5
  Author-email: easytocloud <info@easytocloud.com>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ tests/test_backward_compat.py
43
43
  tests/test_cli_options.py
44
44
  tests/test_config_safety.py
45
45
  tests/test_core.py
46
+ tests/test_instance_tags.py
46
47
  tests/test_non_interactive.py
47
48
  tests/test_runtime_safety.py
48
49
  tests/test_setup.py
@@ -0,0 +1,126 @@
1
+ """get_instance_tags() hostname/environment resolution.
2
+
3
+ Older cloudX-instance.yaml versions set the Name tag to
4
+ cloudX-{env}-{hostname} (optionally " | {username}"); current versions set
5
+ Name to the plain hostname with no prefix. Both must resolve a hostname
6
+ without complaint - the absence of the legacy prefix is not itself a
7
+ problem. Environment must prefer the explicit cloudX:environment tag over
8
+ one parsed out of a legacy Name tag, and only speak up when the two
9
+ sources actively disagree.
10
+ """
11
+
12
+ import pytest
13
+
14
+ from cloudx_proxy.setup import CloudXSetup
15
+
16
+
17
+ class FakeEC2Client:
18
+ def __init__(self, tags):
19
+ self._tags = tags
20
+
21
+ def describe_instances(self, InstanceIds):
22
+ return {
23
+ "Reservations": [
24
+ {"Instances": [{"Tags": [{"Key": k, "Value": v} for k, v in self._tags.items()]}]}
25
+ ]
26
+ }
27
+
28
+
29
+ class FakeSession:
30
+ def __init__(self, tags):
31
+ self._tags = tags
32
+
33
+ def client(self, service_name):
34
+ assert service_name == "ec2"
35
+ return FakeEC2Client(self._tags)
36
+
37
+
38
+ @pytest.fixture
39
+ def setup(tmp_path):
40
+ return CloudXSetup(ssh_config=str(tmp_path / "config"))
41
+
42
+
43
+ def get_tags(setup, monkeypatch, tags):
44
+ monkeypatch.setattr(
45
+ "cloudx_proxy.setup.boto3.Session", lambda *a, **k: FakeSession(tags)
46
+ )
47
+ return setup.get_instance_tags("i-0123456789abcdef0")
48
+
49
+
50
+ class TestPlainNameTag:
51
+ """Current cloudX-instance.yaml: Name is the bare hostname, no prefix."""
52
+
53
+ def test_hostname_from_plain_name_no_warning(self, setup, monkeypatch, capsys):
54
+ environment, hostname = get_tags(
55
+ setup, monkeypatch, {"Name": "erix", "cloudX:environment": "DTA"}
56
+ )
57
+
58
+ assert (environment, hostname) == ("DTA", "erix")
59
+ out = capsys.readouterr().out
60
+ assert "does not match" not in out
61
+ assert "✗" not in out
62
+
63
+ def test_hyphenated_plain_hostname_is_kept_whole(self, setup, monkeypatch):
64
+ environment, hostname = get_tags(
65
+ setup, monkeypatch, {"Name": "my-dev-box", "cloudX:environment": "OTA"}
66
+ )
67
+
68
+ assert (environment, hostname) == ("OTA", "my-dev-box")
69
+
70
+
71
+ class TestLegacyNameTag:
72
+ """Older cloudX-instance.yaml: Name is cloudX-{env}-{hostname}."""
73
+
74
+ def test_env_and_hostname_parsed_when_tag_absent(self, setup, monkeypatch):
75
+ environment, hostname = get_tags(setup, monkeypatch, {"Name": "cloudX-OTA-web1"})
76
+
77
+ assert (environment, hostname) == ("OTA", "web1")
78
+
79
+ def test_username_suffix_is_stripped(self, setup, monkeypatch):
80
+ environment, hostname = get_tags(
81
+ setup, monkeypatch, {"Name": "cloudX-OTA-web1 | erik"}
82
+ )
83
+
84
+ assert (environment, hostname) == ("OTA", "web1")
85
+
86
+
87
+ class TestEnvironmentPrecedence:
88
+ def test_tag_wins_and_conflict_is_flagged(self, setup, monkeypatch, capsys):
89
+ environment, hostname = get_tags(
90
+ setup, monkeypatch, {"Name": "cloudX-FOO-erik", "cloudX:environment": "BAR"}
91
+ )
92
+
93
+ assert environment == "BAR"
94
+ assert hostname == "erik"
95
+ out = capsys.readouterr().out
96
+ assert "FOO" in out and "BAR" in out
97
+ assert "✗" not in out, "a resolved conflict is a warning, not a failure"
98
+
99
+ def test_agreement_is_not_flagged(self, setup, monkeypatch, capsys):
100
+ environment, hostname = get_tags(
101
+ setup, monkeypatch, {"Name": "cloudX-OTA-erik", "cloudX:environment": "OTA"}
102
+ )
103
+
104
+ assert (environment, hostname) == ("OTA", "erik")
105
+ assert "✗" not in capsys.readouterr().out
106
+
107
+ def test_environment_tag_alone_is_sufficient(self, setup, monkeypatch):
108
+ environment, hostname = get_tags(
109
+ setup, monkeypatch, {"Name": "erix", "cloudx:environment": "dta"}
110
+ )
111
+
112
+ assert (environment, hostname) == ("dta", "erix")
113
+
114
+ def test_generic_environment_tag_is_last_resort(self, setup, monkeypatch):
115
+ environment, hostname = get_tags(
116
+ setup, monkeypatch, {"Name": "erix", "Environment": "legacy"}
117
+ )
118
+
119
+ assert (environment, hostname) == ("legacy", "erix")
120
+
121
+
122
+ class TestNothingToGoOn:
123
+ def test_no_name_and_no_environment_tag(self, setup, monkeypatch):
124
+ environment, hostname = get_tags(setup, monkeypatch, {})
125
+
126
+ assert (environment, hostname) == (None, None)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes