Skip to content

Commit 313077d

Browse files
docs(skills): update upgrading-sdk-v2 with ActionError adoption and local CI validation (#36)
* feat: add upgrading-sdk-v2 agent skill for SDK 1.x to 2.0.0 migration Agent skill for AI coding assistants (Amp, Claude Code) that automates upgrading integrations from SDK 1.x to 2.0.0. Covers source code (context.fetch → FetchResponse.data), test mock updates, requirements.txt, and config.json version bump. Includes skills/README.md with setup instructions for workspace-level and global installation. * docs(skills): update upgrading-sdk-v2 with ActionError adoption and local CI validation - Document ActionError (SDK 1.1.0) adoption as part of the upgrade - Document execute_action ValidationError → ResultType change - Make local CI validation a required step with exact commands - Add ruff config caveat (line-length=120 vs default 88) - Add CI fetch pattern linter false-positive caveat - Expand checklist and gotchas sections Closes #35
1 parent 5f34b15 commit 313077d

3 files changed

Lines changed: 337 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ CI runs automatically on PRs via GitHub Actions — see [`.github/workflows/test
5656

5757
Integration validation is handled by the [autohive-integrations-tooling](https://github.com/Autohive-AI/autohive-integrations-tooling) repo. See its README for CI pipeline setup and the integration checklist.
5858

59+
## Agent Skills
60+
61+
The [`skills/`](skills/) directory contains agent skills for AI coding assistants (Amp, Claude Code, etc.) that automate common SDK tasks.
62+
63+
| Skill | Description |
64+
|-------|-------------|
65+
| [`upgrading-sdk-v2`](skills/upgrading-sdk-v2/) | Upgrades an integration from SDK 1.x to 2.0.0 — covers source code, tests, requirements, and config |
66+
67+
To use a skill, copy or symlink it into your workspace's `.agents/skills/` directory or your global `~/.config/agents/skills/` directory. See [`skills/README.md`](skills/README.md) for setup instructions.
68+
5969
## Additional Information
6070

6171
- [Release Notes](RELEASENOTES.md)

skills/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Agent Skills
2+
3+
Agent skills for AI coding assistants (Amp, Claude Code, etc.) that automate common SDK and integration tasks.
4+
5+
## Available Skills
6+
7+
| Skill | Description |
8+
|-------|-------------|
9+
| [`upgrading-sdk-v2/`](upgrading-sdk-v2/) | Upgrades an integration from SDK 1.x to 2.0.0 |
10+
11+
## Setup
12+
13+
Skills must be installed into a location your agent discovers. Two options:
14+
15+
### Option 1: Workspace-level (per-project)
16+
17+
Copy or symlink the skill into your project's `.agents/skills/` directory:
18+
19+
```bash
20+
# From your integrations repo
21+
mkdir -p .agents/skills
22+
cp -r /path/to/integrations-sdk/skills/upgrading-sdk-v2 .agents/skills/
23+
24+
# Or symlink (keeps it up to date with the SDK repo)
25+
ln -s /path/to/integrations-sdk/skills/upgrading-sdk-v2 .agents/skills/upgrading-sdk-v2
26+
```
27+
28+
### Option 2: Global (all projects)
29+
30+
Copy or symlink into your global agent config:
31+
32+
```bash
33+
mkdir -p ~/.config/agents/skills
34+
cp -r /path/to/integrations-sdk/skills/upgrading-sdk-v2 ~/.config/agents/skills/
35+
36+
# Or symlink
37+
ln -s /path/to/integrations-sdk/skills/upgrading-sdk-v2 ~/.config/agents/skills/upgrading-sdk-v2
38+
```
39+
40+
## Usage
41+
42+
Once installed, the skill is automatically available. You can invoke it by:
43+
44+
- Asking your agent to "upgrade this integration to SDK v2"
45+
- Asking to "migrate to SDK 2.0.0"
46+
- Explicitly: "use the upgrading-sdk-v2 skill on the bitly integration"
47+
48+
The agent will load the skill's instructions and follow the step-by-step workflow.

skills/upgrading-sdk-v2/SKILL.md

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
---
2+
name: upgrading-sdk-v2
3+
description: "Upgrades an Autohive integration from SDK 1.0.x or 1.1.x to 2.0.0. Use when asked to upgrade, migrate, or update an integration's SDK version to v2. Covers source code, tests, requirements.txt, and config.json version bump."
4+
---
5+
6+
# Upgrading an Integration to SDK 2.0.0
7+
8+
## What Changed Between 1.0.x and 2.0.0
9+
10+
### SDK 1.1.0 — ActionError (adopt during upgrade)
11+
12+
SDK 1.1.0 introduced `ActionError`, a dedicated error return type that bypasses output schema validation. Integrations still on 1.0.x never adopted it. **During the 2.0.0 upgrade, also convert all error paths to use `ActionError`.**
13+
14+
```python
15+
from autohive_integrations_sdk import ActionError
16+
17+
# Before — 1.0.x error pattern (fails output schema validation)
18+
return ActionResult(data={"error": str(e)}, cost_usd=0.0)
19+
return ActionResult(data={"result": False, "error": str(e), "items": []}, cost_usd=0.0)
20+
21+
# After — ActionError (returns ResultType.ACTION_ERROR, skips schema validation)
22+
return ActionError(message=str(e))
23+
```
24+
25+
`ActionError` is a dataclass, not an exception — **return it, do not raise it.**
26+
27+
Convert ALL of these patterns:
28+
- `return ActionResult(data={"error": ...})``return ActionError(message=...)`
29+
- `return ActionResult(data={"result": False, "error": ..., <extra keys>})``return ActionError(message=...)` (extra keys like `"items": []` are dropped — ActionError only carries a message)
30+
- Exception catch blocks: `return ActionResult(data={"error": str(e)})``return ActionError(message=str(e))`
31+
32+
### SDK 2.0.0 — FetchResponse (breaking change)
33+
34+
SDK 2.0.0 has **one breaking change**: `context.fetch()` now returns a `FetchResponse` object instead of the parsed body directly.
35+
36+
```python
37+
# SDK 1.x — fetch() returns dict/list/str (the body)
38+
response = await context.fetch(url)
39+
response["key"] # response IS the body
40+
41+
# SDK 2.0.0 — fetch() returns FetchResponse
42+
response = await context.fetch(url)
43+
response.data["key"] # body is at .data
44+
response.status # HTTP status code (new)
45+
response.headers # response headers (new)
46+
```
47+
48+
The `FetchResponse` dataclass:
49+
50+
```python
51+
@dataclass
52+
class FetchResponse:
53+
status: int # e.g. 200, 201, 404
54+
headers: Dict[str, str] # response headers
55+
data: Any # parsed JSON body, raw text, or None
56+
```
57+
58+
## Upgrade Workflow
59+
60+
For each integration, follow these steps **in order**. Do not skip steps.
61+
62+
### Step 1 — Read the integration source
63+
64+
Read the main Python file and understand how `context.fetch()` is used. Common patterns:
65+
66+
| 1.x Pattern | 2.0.0 Pattern |
67+
|---|---|
68+
| `response = await context.fetch(url)` then `response["key"]` | `response = await context.fetch(url)` then `response.data["key"]` |
69+
| `response = await context.fetch(url)` then `response.get("key", default)` | `response = await context.fetch(url)` then `response.data.get("key", default)` |
70+
| `return await context.fetch(url)` (returning body directly) | `return (await context.fetch(url)).data` |
71+
| `data = await context.fetch(url)` then `ActionResult(data=data)` | `response = await context.fetch(url)` then `ActionResult(data=response.data)` |
72+
| `isinstance(response, list)` | `isinstance(response.data, list)` |
73+
74+
### Step 2 — Update the source code
75+
76+
**A. FetchResponse — add `.data` to all fetch call sites:**
77+
78+
For every `context.fetch()` call site:
79+
80+
1. If the result is used as a dict/list (accessing keys, iterating), add `.data`
81+
2. If the result is returned directly or passed to `ActionResult(data=...)`, add `.data`
82+
3. If the result is checked with `isinstance()`, check `.data` instead
83+
4. If the result is stored then accessed later, trace all access points
84+
5. If the result is checked with `hasattr(response, "status_code")` or `hasattr(response, "json")`, replace with `.status` and `.data``FetchResponse` always has these fields
85+
86+
**B. ActionError — convert all error returns:**
87+
88+
1. Add `ActionError` to the SDK import: `from autohive_integrations_sdk import ..., ActionError`
89+
2. Convert every `return ActionResult(data={"error": ...})` to `return ActionError(message=...)`
90+
3. Convert every `return ActionResult(data={"result": False, "error": ...})` to `return ActionError(message=...)`
91+
4. Convert every `except Exception as e: return ActionResult(data={"error": str(e)})` to `return ActionError(message=str(e))`
92+
93+
**Do NOT change:**
94+
- Error handling (`try/except`) — exceptions are raised the same way
95+
- The `context.fetch()` call signature — parameters are unchanged
96+
- `ActionResult`, `ActionHandler` — unchanged
97+
98+
**Optionally leverage** `.status` and `.headers` for richer error handling if the integration currently parses status from exception messages.
99+
100+
### Step 3 — Update requirements.txt
101+
102+
Change the SDK pin:
103+
104+
```
105+
# Before
106+
autohive-integrations-sdk~=1.0.2
107+
108+
# After
109+
autohive-integrations-sdk~=2.0.0
110+
```
111+
112+
Keep all other dependencies unchanged.
113+
114+
### Step 4 — Bump config.json version
115+
116+
Increment the **major** version since the SDK dependency is a breaking change:
117+
118+
```json
119+
// Before
120+
"version": "1.0.1"
121+
122+
// After
123+
"version": "2.0.0"
124+
```
125+
126+
If the integration was already at a higher version (e.g. `1.1.0`), bump to `2.0.0`.
127+
128+
### Step 5 — Update unit tests (if they exist)
129+
130+
**A. Wrap fetch mocks in FetchResponse:**
131+
132+
Unit tests that mock `context.fetch` must return `FetchResponse` instead of bare dicts.
133+
134+
```python
135+
from autohive_integrations_sdk import FetchResponse
136+
137+
# Before — 1.x mock
138+
mock_context.fetch.return_value = {"id": 123, "name": "Test"}
139+
140+
# After — 2.0.0 mock
141+
mock_context.fetch.return_value = FetchResponse(
142+
status=200,
143+
headers={},
144+
data={"id": 123, "name": "Test"},
145+
)
146+
```
147+
148+
For every `mock_context.fetch.return_value = ...` in the test file:
149+
1. Wrap the existing value in `FetchResponse(status=200, headers={}, data=...)`
150+
2. For error scenarios returning non-200 responses, use the appropriate status code
151+
3. For `return_value = None` (simulating fetch failure), keep as `None` — the integration handles this before accessing `.data`
152+
4. For `side_effect = Exception(...)` mocks, keep unchanged — exceptions bypass `FetchResponse`
153+
154+
**B. Update error assertions for ActionError:**
155+
156+
Error paths now return `ActionError` instead of `ActionResult` with error data. Test assertions must change:
157+
158+
```python
159+
from autohive_integrations_sdk import FetchResponse, ResultType # noqa: E402
160+
161+
# Before — 1.0.x error assertion
162+
result = await integration.execute_action("some_action", inputs, mock_context)
163+
assert result.result.data["error"] == "Not found"
164+
assert result.result.data["result"] is False
165+
166+
# After — 2.0.0 ActionError assertion
167+
result = await integration.execute_action("some_action", inputs, mock_context)
168+
assert result.type == ResultType.ACTION_ERROR
169+
assert "Not found" in result.result.message
170+
```
171+
172+
**C. Replace `pytest.raises(ValidationError)` with result type checks:**
173+
174+
SDK 2.0.0 changed `execute_action` to no longer raise `ValidationError`. It now returns an `IntegrationResult` with `type=ResultType.VALIDATION_ERROR`:
175+
176+
```python
177+
# Before — 1.0.x
178+
with pytest.raises(ValidationError):
179+
await integration.execute_action("some_action", bad_inputs, mock_context)
180+
181+
# After — 2.0.0
182+
result = await integration.execute_action("some_action", bad_inputs, mock_context)
183+
assert result.type == ResultType.VALIDATION_ERROR
184+
```
185+
186+
Remove `ValidationError` imports and add `ResultType` where needed.
187+
188+
### Step 6 — Update integration tests (if they exist)
189+
190+
Integration tests (`test_*_integration.py`) that use a `live_context` fixture with a real HTTP client need to return `FetchResponse` from their `real_fetch` function:
191+
192+
```python
193+
from autohive_integrations_sdk import FetchResponse
194+
195+
async def real_fetch(url, *, method="GET", json=None, headers=None, **kwargs):
196+
async with aiohttp.ClientSession() as session:
197+
async with session.request(method, url, json=json, headers=headers) as resp:
198+
data = await resp.json(content_type=None)
199+
return FetchResponse(
200+
status=resp.status,
201+
headers=dict(resp.headers),
202+
data=data,
203+
)
204+
```
205+
206+
### Step 7 — Local validation (required before pushing)
207+
208+
Run the **same checks CI runs** locally. Skipping this step will result in CI failures. The tooling repo must be cloned alongside the integrations repo (see [CONTRIBUTING.md](CONTRIBUTING.md) for setup).
209+
210+
**A. Lint and format (must use the CI ruff config):**
211+
212+
```bash
213+
ruff check --fix <integration>
214+
ruff format --config ../autohive-integrations-tooling/ruff.toml <integration>
215+
```
216+
217+
⚠️ **Always use `--config ../autohive-integrations-tooling/ruff.toml`** for formatting. The tooling config uses `line-length = 120`. Running `ruff format` without it uses the default 88-char width and will fail CI.
218+
219+
**B. Run unit tests:**
220+
221+
```bash
222+
source .venv/bin/activate
223+
python -m pytest <integration>/tests/test_*_unit.py -v
224+
```
225+
226+
**C. Run integration tests (if they exist and credentials are available):**
227+
228+
```bash
229+
python -m pytest <integration>/tests/test_*_integration.py -m integration -v
230+
```
231+
232+
**D. Run the CI validation scripts:**
233+
234+
```bash
235+
python ../autohive-integrations-tooling/scripts/validate_integration.py <integration>
236+
python ../autohive-integrations-tooling/scripts/check_code.py <integration>
237+
```
238+
239+
These scripts run the same checks as CI — structure validation, config-code sync, fetch pattern linting, import checks, bandit security scan, and pip-audit. Fix any issues they report before pushing.
240+
241+
**E. Fetch pattern linter caveat:**
242+
243+
The CI fetch pattern linter (`check_fetch_pattern.py`) does a **naive regex match** on variables named `response` accessed with `.get()` or `["..."]`. If a helper function (like `execute_graphql()`) already returns `response.data`, callers hold a plain dict in a variable named `response` — the linter will false-positive on this. Fix by renaming the variable (e.g. `gql_result`, `body`, `data`).
244+
245+
## Checklist
246+
247+
Before considering an integration upgraded, verify:
248+
249+
- [ ] All `context.fetch()` return values access `.data` for the body
250+
- [ ] All error paths return `ActionError(message=...)` instead of `ActionResult` with error data
251+
- [ ] `ActionError` is imported from the SDK
252+
- [ ] `requirements.txt` pins `autohive-integrations-sdk~=2.0.0`
253+
- [ ] `config.json` version is bumped to `2.0.0`
254+
- [ ] Unit test mocks wrap return values in `FetchResponse(...)`
255+
- [ ] Unit test error assertions use `result.type == ResultType.ACTION_ERROR` and `result.result.message`
256+
- [ ] `pytest.raises(ValidationError)` replaced with `result.type == ResultType.VALIDATION_ERROR`
257+
- [ ] Integration test `real_fetch` returns `FetchResponse(...)` (if applicable)
258+
- [ ] `FetchResponse` and `ResultType` are imported where needed
259+
- [ ] All unit tests pass
260+
- [ ] `ruff check` and `ruff format --config ../autohive-integrations-tooling/ruff.toml` pass
261+
- [ ] `validate_integration.py` and `check_code.py` pass
262+
263+
## Common Gotchas
264+
265+
1. **Helper functions that return fetch results**: If a helper like `fetch_json()` returns `await context.fetch(url)`, every caller of that helper is affected. Either update the helper to return `.data`, or update all callers — pick one, be consistent.
266+
267+
2. **Connected account handlers**: These also use `context.fetch()`. Don't forget to update `get_account_info()` methods.
268+
269+
3. **Chained fetches**: Some integrations fetch a resource, extract an ID, then fetch again. Trace the full chain — the first `.data` access often cascades.
270+
271+
4. **Response used as ActionResult data directly**: `ActionResult(data=response)` becomes `ActionResult(data=response.data)`. The response object itself is not serializable.
272+
273+
5. **`None` return values**: Some integrations check `if not response:` after fetch. With `FetchResponse`, this check needs to be `if response is None:` or `if response.data is None:` depending on intent.
274+
275+
6. **Tests with `return_value = None`**: If the integration code checks `if not result:` after a fetch wrapped in try/except that returns `None` on failure, keep the mock as `None` — the code never reaches `.data` on that path.
276+
277+
7. **CI fetch pattern linter false positives**: The linter flags any variable named `response` accessed with `.get()` or `["..."]`. If a helper already unwraps `.data` and returns a plain dict, rename the variable in callers to avoid the match (e.g. `gql_result`, `body`, `api_data`).
278+
279+
8. **Ruff config mismatch**: CI uses `../autohive-integrations-tooling/ruff.toml` with `line-length = 120`. Always pass `--config` when formatting or local results will differ from CI.

0 commit comments

Comments
 (0)