@openhands/extensions 0.8.0 → 0.10.0
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.
- package/.github/workflows/deprecation-check.yml +25 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/catalog/jira-issue-to-pr.json +14 -0
- package/automations/index.js +2 -0
- package/integrations/README.md +25 -0
- package/integrations/catalog/atlassian.json +91 -3
- package/integrations/catalog/datadog.json +33 -2
- package/integrations/catalog/github.json +20 -0
- package/integrations/catalog/microsoft-outlook.json +13 -0
- package/integrations/catalog/microsoft-teams.json +13 -0
- package/integrations/catalog/notion.json +13 -0
- package/integrations/catalog/onedrive.json +13 -0
- package/integrations/catalog/sharepoint.json +13 -0
- package/integrations/catalog/slack.json +14 -0
- package/integrations/catalog/superhuman-mail.json +36 -0
- package/integrations/catalog/vanta.json +55 -0
- package/integrations/catalog-index.js +73 -69
- package/integrations/catalog.schema.json +70 -3
- package/integrations/index.d.ts +49 -3
- package/marketplaces/openhands-extensions.json +10 -0
- package/package.json +1 -1
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +1 -0
- package/plugins/cobol-modernization/skills/cobol-modernization/SKILL.md +9 -0
- package/plugins/migration-scoring/skills/migration-scoring/SKILL.md +11 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +2 -0
- package/plugins/openhands/SKILL.md +7 -0
- package/plugins/release-notes/SKILL.md +9 -0
- package/pyproject.toml +6 -1
- package/python/openhands_extensions/__init__.py +38 -0
- package/python/openhands_extensions/_version.py +2 -2
- package/python/openhands_extensions/deprecation.py +159 -0
- package/python/openhands_extensions/integration_models.py +233 -0
- package/python/openhands_extensions/integrations.py +59 -4
- package/scripts/build-skills-catalog.mjs +1 -1
- package/scripts/check_deprecations.py +488 -0
- package/skills/add-skill/SKILL.md +2 -0
- package/skills/azure-devops/SKILL.md +12 -1
- package/skills/bitbucket-cloud/SKILL.md +11 -0
- package/skills/code-review/SKILL.md +1 -0
- package/skills/datadog/SKILL.md +3 -1
- package/skills/datadog/references/windows.md +45 -0
- package/skills/discord/SKILL.md +2 -0
- package/skills/docker/SKILL.md +5 -1
- package/skills/github/SKILL.md +3 -1
- package/skills/github/references/windows.md +48 -0
- package/skills/github-pr-review/SKILL.md +1 -0
- package/skills/github-pr-review/references/windows.md +47 -0
- package/skills/github-pr-reviewer/SKILL.md +1 -0
- package/skills/github-pr-reviewer/references/windows.md +61 -0
- package/skills/github-repo-monitor/SKILL.md +1 -0
- package/skills/github-repo-monitor/references/windows.md +61 -0
- package/skills/gitlab/SKILL.md +3 -1
- package/skills/incident-retrospective/SKILL.md +2 -0
- package/skills/index.js +36 -30
- package/skills/iterate/SKILL.md +1 -0
- package/skills/iterate/references/windows.md +41 -0
- package/skills/jira-issue-to-pr/.plugin/plugin.json +19 -0
- package/skills/jira-issue-to-pr/README.md +56 -0
- package/skills/jira-issue-to-pr/SKILL.md +216 -0
- package/skills/jira-issue-to-pr/references/setup.md +170 -0
- package/skills/jira-issue-to-pr/scripts/main.py +341 -0
- package/skills/jupyter/SKILL.md +7 -1
- package/skills/kubernetes/SKILL.md +2 -1
- package/skills/kubernetes/references/windows.md +38 -0
- package/skills/linear/SKILL.md +2 -0
- package/skills/linear/references/windows.md +29 -0
- package/skills/linear-triage/SKILL.md +2 -0
- package/skills/notion/SKILL.md +2 -0
- package/skills/notion/references/windows.md +43 -0
- package/skills/openhands-api/SKILL.md +1 -0
- package/skills/openhands-api/references/windows.md +44 -0
- package/skills/openhands-automation/SKILL.md +1 -0
- package/skills/openhands-automation/references/custom-automation.md +20 -0
- package/skills/openhands-automation/references/windows.md +57 -0
- package/skills/pdflatex/SKILL.md +3 -1
- package/skills/research-brief/SKILL.md +2 -0
- package/skills/skill-creator/SKILL.md +1 -0
- package/skills/skill-creator/references/windows.md +30 -0
- package/skills/slack-channel-monitor/SKILL.md +1 -0
- package/skills/slack-channel-monitor/references/windows.md +64 -0
- package/skills/slack-standup-digest/SKILL.md +2 -0
- package/skills/spark-version-upgrade/SKILL.md +2 -1
- package/skills/ssh/SKILL.md +2 -1
- package/skills/ssh/references/windows.md +59 -0
- package/skills/swift-linux/SKILL.md +2 -1
- package/skills/vercel/SKILL.md +5 -0
- package/tests/test_catalog_schema.py +58 -0
- package/tests/test_catalogs.py +6 -1
- package/tests/test_deprecations.py +136 -0
- package/tests/test_integration_catalog_in_sync.py +118 -7
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Static analysis for deprecation deadlines.
|
|
3
|
+
|
|
4
|
+
This is the OpenHands SDK deprecation checker, scoped to the extensions
|
|
5
|
+
package. It scans the SDK's three supported mechanisms: ``@deprecated``,
|
|
6
|
+
``warn_deprecated``, and ``warn_cleanup``. The only policy difference is the
|
|
7
|
+
two-minor-release runway used by extensions.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import sys
|
|
14
|
+
import tomllib
|
|
15
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import date
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
from packaging import version as pkg_version
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# This is intentionally shorter than the SDK's five-release policy: the public
|
|
25
|
+
# extensions API removes deprecated raw catalog helpers after two minor releases.
|
|
26
|
+
DEPRECATION_RUNWAY_MINOR_RELEASES = 2
|
|
27
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class PackageConfig:
|
|
32
|
+
name: str
|
|
33
|
+
pyproject: Path
|
|
34
|
+
source_roots: tuple[Path, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PACKAGES: tuple[PackageConfig, ...] = (
|
|
38
|
+
PackageConfig(
|
|
39
|
+
name="openhands-extensions",
|
|
40
|
+
pyproject=REPO_ROOT / "pyproject.toml",
|
|
41
|
+
source_roots=(REPO_ROOT / "python" / "openhands_extensions",),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class DeprecationRecord:
|
|
48
|
+
identifier: str
|
|
49
|
+
removed_in: str | date | None
|
|
50
|
+
deprecated_in: str | None
|
|
51
|
+
path: Path
|
|
52
|
+
line: int
|
|
53
|
+
kind: Literal["decorator", "warn_call", "cleanup_call"]
|
|
54
|
+
package: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_current_version(pyproject: Path) -> str:
|
|
58
|
+
data = tomllib.loads(pyproject.read_text())
|
|
59
|
+
try:
|
|
60
|
+
return str(data["project"]["version"])
|
|
61
|
+
except KeyError as exc: # pragma: no cover - configuration error
|
|
62
|
+
raise SystemExit(
|
|
63
|
+
f"Unable to determine project version from {pyproject}"
|
|
64
|
+
) from exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _iter_python_files(root: Path) -> Iterator[Path]:
|
|
68
|
+
for path in root.rglob("*.py"):
|
|
69
|
+
if path.name == "__init__.py" and path.parent == root:
|
|
70
|
+
continue
|
|
71
|
+
yield path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_removed_value(
|
|
75
|
+
node: ast.AST | None,
|
|
76
|
+
*,
|
|
77
|
+
path: Path,
|
|
78
|
+
line: int,
|
|
79
|
+
) -> str | date | None:
|
|
80
|
+
if node is None:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
expression = ast.unparse(node)
|
|
84
|
+
|
|
85
|
+
if isinstance(node, ast.Constant):
|
|
86
|
+
if isinstance(node.value, str):
|
|
87
|
+
return node.value
|
|
88
|
+
if node.value is None:
|
|
89
|
+
return None
|
|
90
|
+
raise SystemExit(
|
|
91
|
+
f"Unsupported removed_in literal at {path}:{line}: {expression}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if isinstance(node, ast.Call):
|
|
95
|
+
func = node.func
|
|
96
|
+
if isinstance(func, ast.Name) and func.id == "date":
|
|
97
|
+
try:
|
|
98
|
+
args = [_safe_int_literal(arg) for arg in node.args]
|
|
99
|
+
kwargs = {
|
|
100
|
+
kw.arg: _safe_int_literal(kw.value)
|
|
101
|
+
for kw in node.keywords
|
|
102
|
+
if kw.arg is not None
|
|
103
|
+
}
|
|
104
|
+
except ValueError as exc:
|
|
105
|
+
raise SystemExit(
|
|
106
|
+
f"Unsupported removed_in date() arguments at {path}:{line}:"
|
|
107
|
+
f" {expression}"
|
|
108
|
+
) from exc
|
|
109
|
+
|
|
110
|
+
if any(kw.arg is None for kw in node.keywords):
|
|
111
|
+
raise SystemExit(
|
|
112
|
+
"Unsupported removed_in date() call (uses **kwargs) at "
|
|
113
|
+
f"{path}:{line}: {expression}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
return date(*args, **kwargs)
|
|
118
|
+
except TypeError as exc:
|
|
119
|
+
raise SystemExit(
|
|
120
|
+
f"Invalid removed_in date() call at {path}:{line}: {expression}"
|
|
121
|
+
) from exc
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
isinstance(func, ast.Attribute)
|
|
125
|
+
and isinstance(func.value, ast.Name)
|
|
126
|
+
and func.value.id == "date"
|
|
127
|
+
and func.attr == "today"
|
|
128
|
+
):
|
|
129
|
+
if node.args or node.keywords:
|
|
130
|
+
raise SystemExit(
|
|
131
|
+
"date.today() removed_in call must not include arguments at "
|
|
132
|
+
f"{path}:{line}: {expression}"
|
|
133
|
+
)
|
|
134
|
+
return date.today()
|
|
135
|
+
|
|
136
|
+
raise SystemExit(
|
|
137
|
+
f"Unsupported removed_in expression at {path}:{line}: {expression}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _parse_deprecated_value(
|
|
142
|
+
node: ast.AST | None,
|
|
143
|
+
*,
|
|
144
|
+
path: Path,
|
|
145
|
+
line: int,
|
|
146
|
+
) -> str | None:
|
|
147
|
+
if node is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
expression = ast.unparse(node)
|
|
151
|
+
|
|
152
|
+
if isinstance(node, ast.Constant):
|
|
153
|
+
if isinstance(node.value, str):
|
|
154
|
+
return node.value
|
|
155
|
+
if node.value is None:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
raise SystemExit(
|
|
159
|
+
f"Unsupported deprecated_in expression at {path}:{line}: {expression}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _safe_int_literal(node: ast.AST) -> int:
|
|
164
|
+
if not isinstance(node, ast.Constant) or not isinstance(node.value, int):
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"Unsupported expression inside literal evaluation: {ast.unparse(node)}"
|
|
167
|
+
)
|
|
168
|
+
return node.value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _extract_kw(call: ast.Call, name: str) -> ast.AST | None:
|
|
172
|
+
for kw in call.keywords:
|
|
173
|
+
if kw.arg == name:
|
|
174
|
+
return kw.value
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _gather_decorators(
|
|
179
|
+
tree: ast.AST, path: Path, *, package: str
|
|
180
|
+
) -> Iterator[DeprecationRecord]:
|
|
181
|
+
for node in ast.walk(tree):
|
|
182
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
for deco in node.decorator_list:
|
|
186
|
+
call = deco if isinstance(deco, ast.Call) else None
|
|
187
|
+
if call is None:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
target = call.func
|
|
191
|
+
if isinstance(target, ast.Name):
|
|
192
|
+
decorator_name = target.id
|
|
193
|
+
elif isinstance(target, ast.Attribute):
|
|
194
|
+
decorator_name = target.attr
|
|
195
|
+
else:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if decorator_name != "deprecated":
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
removed_expr = _extract_kw(call, "removed_in")
|
|
202
|
+
deprecated_expr = _extract_kw(call, "deprecated_in")
|
|
203
|
+
|
|
204
|
+
yield DeprecationRecord(
|
|
205
|
+
identifier=_build_identifier(node),
|
|
206
|
+
removed_in=_parse_removed_value(
|
|
207
|
+
removed_expr, path=path, line=node.lineno
|
|
208
|
+
),
|
|
209
|
+
deprecated_in=_parse_deprecated_value(
|
|
210
|
+
deprecated_expr, path=path, line=node.lineno
|
|
211
|
+
),
|
|
212
|
+
path=path,
|
|
213
|
+
line=node.lineno,
|
|
214
|
+
kind="decorator",
|
|
215
|
+
package=package,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _gather_warn_calls(
|
|
220
|
+
tree: ast.AST, path: Path, *, package: str
|
|
221
|
+
) -> Iterator[DeprecationRecord]:
|
|
222
|
+
for node in ast.walk(tree):
|
|
223
|
+
if not isinstance(node, ast.Call):
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
target = node.func
|
|
227
|
+
if isinstance(target, ast.Name):
|
|
228
|
+
func_name = target.id
|
|
229
|
+
elif isinstance(target, ast.Attribute):
|
|
230
|
+
func_name = target.attr
|
|
231
|
+
else:
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if func_name == "warn_deprecated":
|
|
235
|
+
identifier_node = node.args[0] if node.args else None
|
|
236
|
+
if identifier_node is None:
|
|
237
|
+
continue
|
|
238
|
+
identifier = ast.unparse(identifier_node)
|
|
239
|
+
|
|
240
|
+
removed_expr = _extract_kw(node, "removed_in")
|
|
241
|
+
deprecated_expr = _extract_kw(node, "deprecated_in")
|
|
242
|
+
|
|
243
|
+
yield DeprecationRecord(
|
|
244
|
+
identifier=identifier,
|
|
245
|
+
removed_in=_parse_removed_value(
|
|
246
|
+
removed_expr, path=path, line=node.lineno
|
|
247
|
+
),
|
|
248
|
+
deprecated_in=_parse_deprecated_value(
|
|
249
|
+
deprecated_expr, path=path, line=node.lineno
|
|
250
|
+
),
|
|
251
|
+
path=path,
|
|
252
|
+
line=node.lineno,
|
|
253
|
+
kind="warn_call",
|
|
254
|
+
package=package,
|
|
255
|
+
)
|
|
256
|
+
elif func_name == "warn_cleanup":
|
|
257
|
+
identifier_node = node.args[0] if node.args else None
|
|
258
|
+
if identifier_node is None:
|
|
259
|
+
continue
|
|
260
|
+
identifier = ast.unparse(identifier_node)
|
|
261
|
+
|
|
262
|
+
cleanup_expr = _extract_kw(node, "cleanup_by")
|
|
263
|
+
|
|
264
|
+
yield DeprecationRecord(
|
|
265
|
+
identifier=identifier,
|
|
266
|
+
removed_in=_parse_removed_value(
|
|
267
|
+
cleanup_expr, path=path, line=node.lineno
|
|
268
|
+
),
|
|
269
|
+
deprecated_in=None,
|
|
270
|
+
path=path,
|
|
271
|
+
line=node.lineno,
|
|
272
|
+
kind="cleanup_call",
|
|
273
|
+
package=package,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_identifier(node: ast.AST) -> str:
|
|
278
|
+
if isinstance(node, ast.ClassDef):
|
|
279
|
+
return node.name
|
|
280
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
281
|
+
qual_name = node.name
|
|
282
|
+
if node.decorator_list:
|
|
283
|
+
parent = getattr(node, "parent", None)
|
|
284
|
+
if parent and isinstance(parent, ast.ClassDef):
|
|
285
|
+
return f"{parent.name}.{node.name}"
|
|
286
|
+
return qual_name
|
|
287
|
+
return "<unknown>"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _attach_parents(tree: ast.AST) -> None:
|
|
291
|
+
for node in ast.walk(tree):
|
|
292
|
+
for child in ast.iter_child_nodes(node):
|
|
293
|
+
setattr(child, "parent", node)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _collect_records(files: Iterable[Path], *, package: str) -> list[DeprecationRecord]:
|
|
297
|
+
records: list[DeprecationRecord] = []
|
|
298
|
+
for path in files:
|
|
299
|
+
tree = ast.parse(path.read_text())
|
|
300
|
+
_attach_parents(tree)
|
|
301
|
+
records.extend(_gather_decorators(tree, path, package=package))
|
|
302
|
+
records.extend(_gather_warn_calls(tree, path, package=package))
|
|
303
|
+
return records
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _version_ge(current: str, target: str) -> bool:
|
|
307
|
+
try:
|
|
308
|
+
return pkg_version.parse(current) >= pkg_version.parse(target)
|
|
309
|
+
except pkg_version.InvalidVersion as exc:
|
|
310
|
+
raise SystemExit(
|
|
311
|
+
f"Invalid semantic version comparison: {current=} {target=}"
|
|
312
|
+
) from exc
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _minimum_removed_in(deprecated_in: str) -> str:
|
|
316
|
+
parsed = pkg_version.parse(deprecated_in)
|
|
317
|
+
if not isinstance(parsed, pkg_version.Version):
|
|
318
|
+
raise SystemExit(f"Invalid deprecated_in version: {deprecated_in!r}")
|
|
319
|
+
return f"{parsed.major}.{parsed.minor + DEPRECATION_RUNWAY_MINOR_RELEASES}.0"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _runway_error(record: DeprecationRecord) -> str | None:
|
|
323
|
+
if record.kind == "cleanup_call" or record.removed_in is None:
|
|
324
|
+
return None
|
|
325
|
+
if isinstance(record.removed_in, date):
|
|
326
|
+
return None
|
|
327
|
+
if record.deprecated_in is None:
|
|
328
|
+
return (
|
|
329
|
+
f"{record.identifier} does not declare deprecated_in; version-based "
|
|
330
|
+
"deprecations must provide an explicit runway."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
minimum_removed_in = _minimum_removed_in(record.deprecated_in)
|
|
335
|
+
if pkg_version.parse(str(record.removed_in)) >= pkg_version.parse(
|
|
336
|
+
minimum_removed_in
|
|
337
|
+
):
|
|
338
|
+
return None
|
|
339
|
+
except pkg_version.InvalidVersion as exc:
|
|
340
|
+
raise SystemExit(
|
|
341
|
+
f"Invalid deprecation schedule at {record.path}:{record.line}: "
|
|
342
|
+
f"deprecated_in={record.deprecated_in!r}, "
|
|
343
|
+
f"removed_in={record.removed_in!r}"
|
|
344
|
+
) from exc
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
f"{record.identifier} uses an invalid deprecation schedule: "
|
|
348
|
+
f"deprecated_in={record.deprecated_in} and "
|
|
349
|
+
f"removed_in={record.removed_in}. Deprecations require at least "
|
|
350
|
+
f"{DEPRECATION_RUNWAY_MINOR_RELEASES} minor releases of runway "
|
|
351
|
+
f"(minimum removed_in: {minimum_removed_in})."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _should_fail(current_version: str, record: DeprecationRecord) -> bool:
|
|
356
|
+
removed = record.removed_in
|
|
357
|
+
if removed is None:
|
|
358
|
+
return False
|
|
359
|
+
if isinstance(removed, date):
|
|
360
|
+
return date.today() >= removed
|
|
361
|
+
try:
|
|
362
|
+
return _version_ge(current_version, str(removed))
|
|
363
|
+
except SystemExit:
|
|
364
|
+
raise
|
|
365
|
+
except Exception as exc: # pragma: no cover - unexpected literal type
|
|
366
|
+
raise SystemExit(
|
|
367
|
+
f"Unsupported removed_in expression in {record.path}:{record.line}:"
|
|
368
|
+
f" {removed!r}"
|
|
369
|
+
) from exc
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _format_record(record: DeprecationRecord) -> str:
|
|
373
|
+
location = record.path.relative_to(REPO_ROOT)
|
|
374
|
+
removed = record.removed_in if record.removed_in is not None else "(none)"
|
|
375
|
+
|
|
376
|
+
if record.kind == "cleanup_call":
|
|
377
|
+
return (
|
|
378
|
+
f"- [{record.package}] {record.identifier} ({record.kind})\n"
|
|
379
|
+
f" cleanup by: {removed}\n"
|
|
380
|
+
f" defined at: {location}:{record.line}"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
deprecated = (
|
|
384
|
+
record.deprecated_in if record.deprecated_in is not None else "(unknown)"
|
|
385
|
+
)
|
|
386
|
+
return (
|
|
387
|
+
f"- [{record.package}] {record.identifier} ({record.kind})\n"
|
|
388
|
+
f" deprecated in: {deprecated}\n"
|
|
389
|
+
f" removed in: {removed}\n"
|
|
390
|
+
f" defined at: {location}:{record.line}"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
395
|
+
del argv # The SDK checker takes no command-line configuration.
|
|
396
|
+
|
|
397
|
+
overdue: list[DeprecationRecord] = []
|
|
398
|
+
invalid_runway: list[tuple[DeprecationRecord, str]] = []
|
|
399
|
+
total_records = 0
|
|
400
|
+
package_summaries: list[tuple[str, str, int]] = []
|
|
401
|
+
|
|
402
|
+
for package in PACKAGES:
|
|
403
|
+
if not package.pyproject.exists():
|
|
404
|
+
raise SystemExit(
|
|
405
|
+
f"Unable to locate pyproject.toml for {package.name}: "
|
|
406
|
+
f"{package.pyproject}"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
current_version = _load_current_version(package.pyproject)
|
|
410
|
+
|
|
411
|
+
files: list[Path] = []
|
|
412
|
+
for root in package.source_roots:
|
|
413
|
+
if not root.exists():
|
|
414
|
+
raise SystemExit(
|
|
415
|
+
f"Source root {root} for package {package.name} does not exist"
|
|
416
|
+
)
|
|
417
|
+
files.extend(_iter_python_files(root))
|
|
418
|
+
|
|
419
|
+
records = _collect_records(files, package=package.name)
|
|
420
|
+
overdue.extend(r for r in records if _should_fail(current_version, r))
|
|
421
|
+
invalid_runway.extend(
|
|
422
|
+
(record, error)
|
|
423
|
+
for record in records
|
|
424
|
+
if (error := _runway_error(record)) is not None
|
|
425
|
+
)
|
|
426
|
+
total_records += len(records)
|
|
427
|
+
package_summaries.append((package.name, current_version, len(records)))
|
|
428
|
+
|
|
429
|
+
if overdue or invalid_runway:
|
|
430
|
+
deprecated_items = [r for r in overdue if r.kind != "cleanup_call"]
|
|
431
|
+
cleanup_items = [r for r in overdue if r.kind == "cleanup_call"]
|
|
432
|
+
|
|
433
|
+
if invalid_runway:
|
|
434
|
+
print(
|
|
435
|
+
"The following deprecation schedules provide less than "
|
|
436
|
+
f"{DEPRECATION_RUNWAY_MINOR_RELEASES} minor releases of runway:\n"
|
|
437
|
+
)
|
|
438
|
+
for record, error in invalid_runway:
|
|
439
|
+
print(_format_record(record))
|
|
440
|
+
print(f" error: {error}")
|
|
441
|
+
print()
|
|
442
|
+
|
|
443
|
+
if deprecated_items:
|
|
444
|
+
print(
|
|
445
|
+
"The following deprecated features have passed their removal "
|
|
446
|
+
"deadline:\n"
|
|
447
|
+
)
|
|
448
|
+
for record in deprecated_items:
|
|
449
|
+
print(_format_record(record))
|
|
450
|
+
print()
|
|
451
|
+
|
|
452
|
+
if cleanup_items:
|
|
453
|
+
print("The following workarounds have passed their cleanup deadline:\n")
|
|
454
|
+
for record in cleanup_items:
|
|
455
|
+
print(_format_record(record))
|
|
456
|
+
print()
|
|
457
|
+
|
|
458
|
+
if deprecated_items:
|
|
459
|
+
print(
|
|
460
|
+
"Update or remove the listed features before publishing a version that "
|
|
461
|
+
"meets or exceeds their removal deadline."
|
|
462
|
+
)
|
|
463
|
+
if cleanup_items:
|
|
464
|
+
print(
|
|
465
|
+
"Remove the listed workarounds before publishing a version that "
|
|
466
|
+
"meets or exceeds their cleanup deadline."
|
|
467
|
+
)
|
|
468
|
+
if invalid_runway:
|
|
469
|
+
print(
|
|
470
|
+
"Move each removal target far enough out, or remove the invalid "
|
|
471
|
+
"deprecation metadata before merging."
|
|
472
|
+
)
|
|
473
|
+
return 1
|
|
474
|
+
|
|
475
|
+
for package_name, version, count in package_summaries:
|
|
476
|
+
print(
|
|
477
|
+
f"{package_name}: checked {count} deprecation metadata entries against "
|
|
478
|
+
f"version {version}."
|
|
479
|
+
)
|
|
480
|
+
print(
|
|
481
|
+
f"Checked {total_records} deprecation metadata entries across "
|
|
482
|
+
f"{len(package_summaries)} package(s)."
|
|
483
|
+
)
|
|
484
|
+
return 0
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == "__main__": # pragma: no cover - manual invocation
|
|
488
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -38,6 +38,8 @@ python3 scripts/fetch_skill.py "https://github.com/OpenHands/extensions/tree/mai
|
|
|
38
38
|
ls /path/to/workspace/.agents/skills/codereview/SKILL.md
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
On Windows, use `python` if `python3` is not available and verify with PowerShell, for example: `Test-Path C:\path\to\workspace\.agents\skills\codereview\SKILL.md`.
|
|
42
|
+
|
|
41
43
|
Response: "✅ Added `codereview` to your workspace. The skill is now available."
|
|
42
44
|
|
|
43
45
|
## Notes
|
|
@@ -30,6 +30,8 @@ git remote -v && git branch # to find the current org, repo and branch
|
|
|
30
30
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
On Windows PowerShell, run those `git` commands as separate commands if `&&` is not supported by the installed shell.
|
|
34
|
+
|
|
33
35
|
## Azure DevOps API Usage
|
|
34
36
|
|
|
35
37
|
When working with Azure DevOps API, you need to use Basic authentication with your Personal Access Token (PAT). The username is ignored (empty string), and the password is the PAT.
|
|
@@ -43,8 +45,17 @@ AUTH=$(echo -n ":$AZURE_DEVOPS_TOKEN" | base64)
|
|
|
43
45
|
curl -H "Authorization: Basic $AUTH" -H "Content-Type: application/json" https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
PowerShell equivalent for the PAT header:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$env:AZURE_DEVOPS_TOKEN"))
|
|
52
|
+
Invoke-RestMethod `
|
|
53
|
+
-Headers @{ Authorization = "Basic $auth"; "Content-Type" = "application/json" } `
|
|
54
|
+
-Uri "https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1"
|
|
55
|
+
```
|
|
56
|
+
|
|
46
57
|
Common API endpoints:
|
|
47
58
|
- List repositories: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1`
|
|
48
59
|
- Get repository details: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}?api-version=7.1`
|
|
49
60
|
- List pull requests: `https://dev.azure.com/{organization}/{project}/_apis/git/pullrequests?api-version=7.1`
|
|
50
|
-
- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
|
|
61
|
+
- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
|
|
@@ -28,6 +28,15 @@ ENCODED_PASS=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sy
|
|
|
28
28
|
git remote set-url origin "https://${ENCODED_USER}:${ENCODED_PASS}@bitbucket.org/username/repo.git"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
PowerShell equivalent for the remote URL encoding:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
$parts = $env:BITBUCKET_TOKEN -split ":", 2
|
|
35
|
+
$encodedUser = [Uri]::EscapeDataString($parts[0])
|
|
36
|
+
$encodedPass = [Uri]::EscapeDataString($parts[1])
|
|
37
|
+
git remote set-url origin "https://${encodedUser}:${encodedPass}@bitbucket.org/username/repo.git"
|
|
38
|
+
```
|
|
39
|
+
|
|
31
40
|
Atlassian's Bitbucket Cloud docs recommend avoiding long-lived credentials in the remote URL when possible. Their API token examples use either `https://{bitbucket_username}:{api_token}@...` or `https://x-bitbucket-api-token-auth:{api_token}@...`; OpenHands users should only construct those URLs on demand, with proper URL encoding.
|
|
32
41
|
|
|
33
42
|
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
|
@@ -44,3 +53,5 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
|
|
44
53
|
git remote -v && git branch # to find the current org, repo and branch
|
|
45
54
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
46
55
|
```
|
|
56
|
+
|
|
57
|
+
On Windows PowerShell, run those `git` commands as separate commands if `&&` is not supported by the installed shell.
|
|
@@ -35,6 +35,7 @@ The prompt includes a **Files Changed** manifest listing every file in the PR, f
|
|
|
35
35
|
3. Only after both checks come up empty should you flag something as missing. Even then, prefer "I could not locate X" over "X is missing" — the file may be in a path you haven't searched.
|
|
36
36
|
|
|
37
37
|
Before posting an **inline review comment that names a specific line number**, verify the line maps to what you think it does (`sed -n 'X,Yp' <file>` or `view`). Line numbers derived by counting `+`/`-`/context lines from a `@@` hunk header are not reliable; ground them against the file.
|
|
38
|
+
On Windows PowerShell, use `Get-Content`, `Select-String`, or `(Get-Content <file>)[($start - 1)..($end - 1)]` for the same file and line checks.
|
|
38
39
|
|
|
39
40
|
CODE REVIEW SCENARIOS:
|
|
40
41
|
|
package/skills/datadog/SKILL.md
CHANGED
|
@@ -7,6 +7,8 @@ triggers:
|
|
|
7
7
|
|
|
8
8
|
# Datadog
|
|
9
9
|
|
|
10
|
+
Windows PowerShell equivalents for the Datadog `curl`, environment-variable, timestamp, and JSON formatting snippets are in `references/windows.md`.
|
|
11
|
+
|
|
10
12
|
<IMPORTANT>
|
|
11
13
|
Before performing any Datadog operations, first check if the required environment variables are set:
|
|
12
14
|
|
|
@@ -92,4 +94,4 @@ curl -s -G "https://api.${DD_SITE}/api/v1/monitor" \
|
|
|
92
94
|
- [APM/Tracing API](https://docs.datadoghq.com/api/latest/tracing/)
|
|
93
95
|
- [Monitors API](https://docs.datadoghq.com/api/latest/monitors/)
|
|
94
96
|
- [Events API](https://docs.datadoghq.com/api/latest/events/)
|
|
95
|
-
- [Dashboards API](https://docs.datadoghq.com/api/latest/dashboards/)
|
|
97
|
+
- [Dashboards API](https://docs.datadoghq.com/api/latest/dashboards/)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Windows Notes for Datadog Commands
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms when running the Datadog examples natively on Windows.
|
|
4
|
+
|
|
5
|
+
## Check Required Environment Variables
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
foreach ($name in "DD_API_KEY", "DD_APP_KEY", "DD_SITE") {
|
|
9
|
+
if ([Environment]::GetEnvironmentVariable($name)) {
|
|
10
|
+
"$name is set"
|
|
11
|
+
} else {
|
|
12
|
+
"$name is NOT set"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Run the Curl Examples
|
|
18
|
+
|
|
19
|
+
In PowerShell, call `curl.exe` so the examples use real curl rather than the `curl` alias. Replace `${DD_SITE}` with `$env:DD_SITE` and `${DD_API_KEY}` / `${DD_APP_KEY}` with `$env:DD_API_KEY` / `$env:DD_APP_KEY`.
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
curl.exe -s -X POST "https://api.$env:DD_SITE/api/v2/logs/events/search" `
|
|
23
|
+
-H "DD-API-KEY: $env:DD_API_KEY" `
|
|
24
|
+
-H "DD-APPLICATION-KEY: $env:DD_APP_KEY" `
|
|
25
|
+
-H "Content-Type: application/json" `
|
|
26
|
+
-d '{"filter":{"query":"service:my-service status:error","from":"now-1h","to":"now"},"sort":"-timestamp","page":{"limit":50}}' |
|
|
27
|
+
ConvertFrom-Json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Replace Linux Date and Jq
|
|
31
|
+
|
|
32
|
+
The metrics example uses `date -d` and `jq`. In PowerShell, compute Unix timestamps with .NET and pipe JSON to `ConvertFrom-Json`.
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
$from = [DateTimeOffset]::UtcNow.AddHours(-1).ToUnixTimeSeconds()
|
|
36
|
+
$to = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
37
|
+
|
|
38
|
+
curl.exe -s -G "https://api.$env:DD_SITE/api/v1/query" `
|
|
39
|
+
-H "DD-API-KEY: $env:DD_API_KEY" `
|
|
40
|
+
-H "DD-APPLICATION-KEY: $env:DD_APP_KEY" `
|
|
41
|
+
--data-urlencode "query=avg:system.cpu.user{*}" `
|
|
42
|
+
--data-urlencode "from=$from" `
|
|
43
|
+
--data-urlencode "to=$to" |
|
|
44
|
+
ConvertFrom-Json
|
|
45
|
+
```
|
package/skills/discord/SKILL.md
CHANGED
|
@@ -49,6 +49,8 @@ Use this skill when implementing or automating Discord integrations.
|
|
|
49
49
|
|
|
50
50
|
## Quick recipes
|
|
51
51
|
|
|
52
|
+
The shell snippets below use POSIX-style environment variables and line continuations. On Windows PowerShell, use `curl.exe` for the shown flags and `$env:DISCORD_WEBHOOK_URL` / `$env:DISCORD_BOT_TOKEN` for environment variables, or translate the request to `Invoke-RestMethod`.
|
|
53
|
+
|
|
52
54
|
### Post a message via an incoming webhook (recommended)
|
|
53
55
|
|
|
54
56
|
Discord requires at least one of `content`, `embeds`, `components`, `file`, or `poll`.
|
package/skills/docker/SKILL.md
CHANGED
|
@@ -20,10 +20,14 @@ sudo dockerd > /tmp/docker.log 2>&1 &
|
|
|
20
20
|
sleep 5
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
On Windows, start Docker Desktop or the Docker service instead of running `sudo dockerd`; then run Docker commands from PowerShell without `sudo`.
|
|
24
|
+
|
|
23
25
|
## Verifying Docker Installation
|
|
24
26
|
|
|
25
27
|
To verify Docker is working correctly, run the hello-world container:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
30
|
sudo docker run hello-world
|
|
29
|
-
```
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
PowerShell equivalent after Docker Desktop is running: `docker run hello-world`.
|
package/skills/github/SKILL.md
CHANGED
|
@@ -19,6 +19,8 @@ Examples:
|
|
|
19
19
|
- `gh pr checks 200 --watch --interval 10` to check until completed.
|
|
20
20
|
</IMPORTANT>
|
|
21
21
|
|
|
22
|
+
Windows PowerShell equivalents for the multi-line shell snippets below are in `references/windows.md`.
|
|
23
|
+
|
|
22
24
|
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
|
|
23
25
|
|
|
24
26
|
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
|
@@ -103,4 +105,4 @@ gh run list --repo <OWNER>/<REPO> --branch <BRANCH> --limit 5
|
|
|
103
105
|
|
|
104
106
|
# Rerun failed jobs
|
|
105
107
|
gh run rerun <RUN_ID> --repo <OWNER>/<REPO> --failed
|
|
106
|
-
```
|
|
108
|
+
```
|