Skip to content

feat(salesforce): add Salesforce CRM integration#270

Closed
Shubhank-Jonnada wants to merge 6 commits intomasterfrom
feat/salesforce-integration-v2
Closed

feat(salesforce): add Salesforce CRM integration#270
Shubhank-Jonnada wants to merge 6 commits intomasterfrom
feat/salesforce-integration-v2

Conversation

@Shubhank-Jonnada
Copy link
Copy Markdown
Contributor

Summary

Adds a brand new Salesforce CRM integration covering the core use cases of searching/updating records and summarising task & event activity.

Actions (7)

Action Description
search_records Run a SOQL query against any Salesforce object (Contact, Lead, Opportunity, etc.)
get_record Fetch a single record by ID and object type
update_record Update fields on any record via PATCH
list_tasks List Task records with optional status, date, and owner filters
list_events List Event records with optional date and owner filters
get_task_summary Retrieve a Task by ID and return a human-readable summary
get_event_summary Retrieve an Event by ID and return a human-readable summary

Auth

  • OAuth 2.0 via Salesforce Connected App (platform auth type)
  • instance_url resolved from context.metadata — Salesforce returns this at token time and the platform stores it separately from credentials

Tests

  • 56 pytest unit tests, fully mocked, zero credentials required (pytest -m unit)
  • Integration test file included for live credential testing (pytest -m integration)

Test plan

  • pytest salesforce/tests/test_salesforce_unit.py -v — all 56 pass
  • python autohive-integrations-tooling/scripts/validate_integration.py salesforce — no errors
  • python autohive-integrations-tooling/scripts/check_code.py salesforce — no errors
  • Connect a Salesforce Developer Edition account via OAuth in Autohive
  • Test search_records with SELECT Id, Name FROM Contact LIMIT 5
  • Test list_tasks and list_events with no filters
  • Test get_task_summary / get_event_summary with a valid ID

All action handler execute() methods in integration-google-chat and
integration-google-sheets were returning plain dicts. Wrapped every
return value in ActionResult(data=..., cost_usd=0.0) to match the
expected SDK pattern used across other integrations.
…esult

All action handler execute() methods in integration-youtube and
integration-google-business-profile now return ActionResult(data=..., cost_usd=0.0)
instead of plain dicts, conforming to the SDK contract.
- 7 actions: search_records, get_record, update_record, list_tasks,
  list_events, get_task_summary, get_event_summary
- OAuth 2.0 platform auth, instance_url resolved from context.metadata
- 56 pytest unit tests, zero credentials required
- Real Salesforce logo icon (512x512)
- README with auth setup, actions table, troubleshooting
@github-actions
Copy link
Copy Markdown

🔍 Integration Validation Results

Commit: 04e818f2042392ae53493cb0719d8e97906e13e4 · feat(salesforce): add Salesforce CRM integration with SDK 2.0.0
Updated: 2026-04-24T03:34:28Z

Changed directories: salesforce shotstack

Check Result
Structure ❌ Failed
Code ❌ Failed
Tests ⚠️ Passed with warnings
README ✅ Passed
Version ⚠️ Passed with warnings
❌ Structure Check output
Validating 2 integration(s)...

============================================================
Integration: salesforce
============================================================
✅ All checks passed!

============================================================
Integration: shotstack
============================================================

Errors (5):
  ❌ Missing required file: config.json (Integration configuration file)
  ❌ Missing required file: requirements.txt (Python dependencies file)
  ❌ Missing required file: README.md (Integration documentation)
  ❌ Missing required file: icon.png or icon.svg (Integration icon)
  ❌ Missing 'tests/' folder

Warnings (1):
  ⚠️ Missing __init__.py (required for package-style integrations, optional for modular integrations with actions/)

============================================================
SUMMARY
============================================================
Integrations validated: 2
Total errors: 5
Total warnings: 1

❌ Validation FAILED - please fix errors before submitting PR
❌ Code Check output
----------------------------------------
Checking: salesforce
----------------------------------------

📦 Installing dependencies...

🐍 Checking Python syntax...
   ✅ Syntax OK

📥 Checking imports...
   ✅ Imports OK

📄 Checking JSON files...
   ✅ JSON files OK

🔍 Linting with ruff...
   ✅ Lint OK

🎨 Checking formatting with ruff...
   ✅ Formatting OK

🔒 Scanning for security issues with bandit...
   ✅ Security OK

🛡️ Checking dependencies for vulnerabilities with pip-audit...
   ✅ Dependencies OK

🔗 Checking config-code sync...
   ✅ Config-code sync OK

🔄 Checking fetch patterns...
   ✅ Fetch patterns OK

----------------------------------------
Checking: shotstack
----------------------------------------

🐍 Checking Python syntax...
   ✅ Syntax OK

📥 Checking imports...
   ⚠️ No config.json found, skipping

📄 Checking JSON files...
   ✅ JSON files OK

🔍 Linting with ruff...
   E501 Line too long (129 > 120)
     --> shotstack/shotstack.py:29:121
      |
   29 | async def _poll_render(context: ExecutionContext, render_id: str, max_wait: int = 300, poll_interval: int = 5) -> Dict[str, Any]:
      |                                                                                                                         ^^^^^^^^^
   30 |     env = _get_env(context)
   31 |     elapsed = 0
      |
   
   E501 Line too long (128 > 120)
     --> shotstack/shotstack.py:33:121
      |
   31 |     elapsed = 0
   32 |     while elapsed < max_wait:
   33 |         response = await context.fetch(f"{EDIT_API_BASE}/{env}/render/{render_id}", method="GET", headers=_get_headers(context))
      |                                                                                                                         ^^^^^^^^
   34 |         render_data = response.get("response", {})
   35 |         status = render_data.get("status")
      |
   
   E501 Line too long (129 > 120)
     --> shotstack/shotstack.py:45:121
      |
   45 | async def _poll_source(context: ExecutionContext, source_id: str, max_wait: int = 120, poll_interval: int = 3) -> Dict[str, Any]:
      |                                                                                                                         ^^^^^^^^^
   46 |     env = _get_env(context)
   47 |     elapsed = 0
      |
   
   E501 Line too long (131 > 120)
     --> shotstack/shotstack.py:49:121
      |
   47 |     elapsed = 0
   48 |     while elapsed < max_wait:
   49 |         response = await context.fetch(f"{INGEST_API_BASE}/{env}/sources/{source_id}", method="GET", headers=_get_headers(context))
      |                                                                                                                         ^^^^^^^^^^^
   50 |         source_data = response.get("data", {})
   51 |         attributes = source_data.get("attributes", {})
      |
   
   E501 Line too long (151 > 120)
     --> shotstack/shotstack.py:73:121
      |
   71 | …
   72 | …
   73 | …), "content_type": content_type, "filename": filename, "size": len(content_bytes)}
      |                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      |
   
   E501 Line too long (125 > 120)
     --> shotstack/shotstack.py:79:121
      |
   77 |     env = _get_env(context)
   78 |     encoded_url = quote(url, safe="")
   79 |     response = await context.fetch(f"{EDIT_API_BASE}/{env}/probe/{encoded_url}", method="GET", headers=_get_headers(context))
      |                                                                                                                         ^^^^^
   80 |     return response.get("response", {})
      |
   
   E501 Line too long (134 > 120)
      --> shotstack/shotstack.py:124:121
       |
   124 | async def _submit_and_maybe_wait(context: ExecutionContext, payload: Dict[str, Any], wait: bool, max_wait: int = 300) -> ActionResult:
       |                                                                                                                         ^^^^^^^^^^^^^^
   125 |     env = _get_env(context)
   126 |     response = await context.fetch(f"{EDIT_API_BASE}/{env}/render", method="POST", headers=_get_headers(context), json=payload)
       |
   
   E501 Line too long (127 > 120)
      --> shotstack/shotstack.py:126:121
       |
   124 | async def _submit_and_maybe_wait(context: ExecutionContext, payload: Dict[str, Any], wait: bool, max_wait: int = 300) -> ActionResult:
   125 |     env = _get_env(context)
   126 |     response = await context.fetch(f"{EDIT_API_BASE}/{env}/render", method="POST", headers=_get_headers(context), json=payload)
       |                                                                                                                         ^^^^^^^
   127 |     render_id = response.get("response", {}).get("id")
   128 |     if not render_id:
       |
   
   E501 Line too long (178 > 120)
      --> shotstack/shotstack.py:134:121
       |
   132 | …
   133 | …
   134 | …l": poll_result["url"], "duration": render_data.get("duration"), "result": True}, cost_usd=0.0)
       |                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   135 | …atus"], "error": poll_result.get("error"), "result": False}, cost_usd=0.0)
   136 | … True}, cost_usd=0.0)
       |
   
   E501 Line too long (157 > 120)
      --> shotstack/shotstack.py:135:121
       |
   133 | …
   134 | …done", "url": poll_result["url"], "duration": render_data.get("duration"), "result": True}, cost_usd=0.0)
   135 | …result["status"], "error": poll_result.get("error"), "result": False}, cost_usd=0.0)
       |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   136 | … "result": True}, cost_usd=0.0)
       |
   
   E501 Line too long (127 > 120)
      --> shotstack/shotstack.py:159:121
       |
   157 |                 content_type = inputs.get("content_type")
   158 |             if not content_base64 or not filename:
   159 |                 return ActionResult(data={"result": False, "error": "Missing required file content or filename"}, cost_usd=0.0)
       |                                                                                                                         ^^^^^^^
   160 |             file_bytes = base64.b64decode(content_base64)
   161 |             if not content_type:
       |
   
   E501 Line too long (123 > 120)
      --> shotstack/shotstack.py:164:121
       |
   162 |                 guessed_type, _ = mimetypes.guess_type(filename)
   163 |                 content_type = guessed_type or "application/octet-stream"
   164 |             response = await context.fetch(f"{INGEST_API_BASE}/{env}/upload", method="POST", headers=_get_headers(context))
       |                                                                                                                         ^^^
   165 |             upload_data = response.get("data", {})
   166 |             attributes = upload_data.get("attributes", {})
       |
   
   E501 Line too long (160 > 120)
      --> shotstack/shotstack.py:177:121
       |
   175 | …
   176 | …
   177 | …rce_url": poll_result["source_url"], "status": "ready", "result": True}, cost_usd=0.0)
       |                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   178 | …: poll_result["status"], "error": poll_result.get("error"), "result": False}, cost_usd=0.0)
   179 | …rocessing", "result": True}, cost_usd=0.0)
       |
   
   E501 Line too long (165 > 120)
      --> shotstack/shotstack.py:178:121
       |
   176 | …
   177 | …_url": poll_result["source_url"], "status": "ready", "result": True}, cost_usd=0.0)
   178 | …oll_result["status"], "error": poll_result.get("error"), "result": False}, cost_usd=0.0)
       |                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   179 | …essing", "result": True}, cost_usd=0.0)
   180 | …
       |
   
   E501 Line too long (135 > 120)
      --> shotstack/shotstack.py:190:121
       |
   188 | …
   189 | …
   190 | …/{env}/sources/{source_id}", method="GET", headers=_get_headers(context))
       |                                                            ^^^^^^^^^^^^^^^
   191 | …
   192 | …
       |
   
   E501 Line too long (123 > 120)
      --> shotstack/shotstack.py:213:121
       |
   211 |         try:
   212 |             env = _get_env(context)
   213 |             response = await context.fetch(f"{INGEST_API_BASE}/{env}/upload", method="POST", headers=_get_headers(context))
       |                                                                                                                         ^^^
   214 |             upload_data = response.get("data", {})
   215 |             attributes = upload_data.get("attributes", {})
       |
   
   E501 Line too long (179 > 120)
      --> shotstack/shotstack.py:216:121
       |
   214 | …
   215 | …
   216 | …d": upload_data.get("id"), "expires": attributes.get("expires"), "result": True}, cost_usd=0.0)
       |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   217 | …
   218 | …": False, "error": str(e)}, cost_usd=0.0)
       |
   
   E501 Line too long (125 > 120)
      --> shotstack/shotstack.py:218:121
       |
   216 | …         return ActionResult(data={"upload_url": attributes.get("url"), "source_id": upload_data.get("id"), "expires": attributes.ge…
   217 | …     except Exception as e:
   218 | …         return ActionResult(data={"upload_url": None, "source_id": None, "result": False, "error": str(e)}, cost_usd=0.0)
       |                                                                                                                       ^^^^^
       |
   
   E501 Line too long (135 > 120)
      --> shotstack/shotstack.py:227:121
       |
   225 | …
   226 | …": inputs["output"]}
   227 | …env}/render", method="POST", headers=_get_headers(context), json=payload)
       |                                                            ^^^^^^^^^^^^^^^
   228 | …
   229 | …
       |
   
   E501 Line too long (196 > 120)
      --> shotstack/shotstack.py:231:121
       |
   229 | …
   230 | … job"}, cost_usd=0.0)
   231 | … "Render job submitted. Use check_render_status to poll for completion.", "result": True}, cost_usd=0.0)
       |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   232 | …
   233 | …
       |
   
   E501 Line too long (132 > 120)
      --> shotstack/shotstack.py:242:121
       |
   240 |             env = _get_env(context)
   241 |             render_id = inputs["render_id"]
   242 |             response = await context.fetch(f"{EDIT_API_BASE}/{env}/render/{render_id}", method="GET", headers=_get_headers(context))
       |                                                                                                                         ^^^^^^^^^^^^
   243 |             render_data = response.get("response", {})
   244 |             status = render_data.get("status")
       |
   
   E501 Line too long (135 > 120)
      --> shotstack/shotstack.py:268:121
       |
   266 | …, 5)
   267 | …
   268 | …env}/render", method="POST", headers=_get_headers(context), json=payload)
       |                                                            ^^^^^^^^^^^^^^^
   269 | …
   270 | …
       |
   
   E501 Line too long (200 > 120)
      --> shotstack/shotstack.py:274:121
       |
   272 | …
   273 | …
   274 | …l_result["url"], "duration": poll_result.get("render", {}).get("duration"), "result": True}, cost_usd=0.0)
       |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   275 | … "error": poll_result.get("error"), "result": False}, cost_usd=0.0)
   276 | …
       |
   
   E501 Line too long (161 > 120)
      --> shotstack/shotstack.py:275:121
       |
   273 | …
   274 | … "done", "url": poll_result["url"], "duration": poll_result.get("render", {}).get("duration"), "result": True}, cost_usd=0.0)
   275 | …l_result["status"], "error": poll_result.get("error"), "result": False}, cost_usd=0.0)
       |                                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   276 | …
   277 | …st_usd=0.0)
       |
   
   E501 Line too long (136 > 120)
      --> shotstack/shotstack.py:288:121
       |
   286 | …
   287 | …
   288 | …E}/{env}/render/{render_id}", method="GET", headers=_get_headers(context))
       |                                                            ^^^^^^^^^^^^^^^^
   289 | …
   290 | …
       |
   
   E501 Line too long (131 > 120)
      --> shotstack/shotstack.py:292:121
       |
   290 |                 status = render_data.get("status")
   291 |                 if status != "done":
   292 |                     return ActionResult(data={"result": False, "error": f"Render is not complete. Status: {status}"}, cost_usd=0.0)
       |                                                                                                                         ^^^^^^^^^^^
   293 |                 url = render_data.get("url")
   294 |             if not url:
       |
   
   E501 Line too long (129 > 120)
      --> shotstack/shotstack.py:295:121
       |
   293 | …         url = render_data.get("url")
   294 | …     if not url:
   295 | …         return ActionResult(data={"result": False, "error": "No URL available. Provide render_id or url."}, cost_usd=0.0)
       |                                                                                                                   ^^^^^^^^^
   296 | …     dl = await _download_base64(context, url)
   297 | …     return ActionResult(data={"content": dl["content"], "content_type": dl["content_type"], "filename": dl["filename"], "size": dl[…
       |
   
   E501 Line too long (178 > 120)
      --> shotstack/shotstack.py:297:121
       |
   295 | …le. Provide render_id or url."}, cost_usd=0.0)
   296 | …
   297 | …"content_type"], "filename": dl["filename"], "size": dl["size"], "result": True}, cost_usd=0.0)
       |                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   298 | …
   299 | ….0)
       |
   
   E501 Line too long (146 > 120)
      --> shotstack/shotstack.py:352:121
       |
   350 | …
   351 | …
   352 | … text, "style": style, "color": color, "size": font_size, "position": position}
       |                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^
   353 | …
   354 | …
       |
   
   E501 Line too long (132 > 120)
      --> shotstack/shotstack.py:360:121
       |
   358 |             if transition:
   359 |                 text_clip["transition"] = transition
   360 |             timeline = {"tracks": [{"clips": [text_clip]}, {"clips": [{"asset": {"type": "video", "src": video_url}, "start": 0}]}]}
       |                                                                                                                         ^^^^^^^^^^^^
   361 |             return await _submit_and_maybe_wait(context, {"timeline": timeline, "output": output}, wait)
   362 |         except Exception as e:
       |
   
   E501 Line too long (184 > 120)
      --> shotstack/shotstack.py:388:121
       |
   386 | …
   387 | …
   388 | …"start": start_time, "length": duration, "scale": scale, "position": position, "opacity": opacity}
       |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   389 | …
   390 | …
       |
   
   E501 Line too long (132 > 120)
      --> shotstack/shotstack.py:396:121
       |
   394 |                     offset["y"] = offset_y
   395 |                 logo_clip["offset"] = offset
   396 |             timeline = {"tracks": [{"clips": [logo_clip]}, {"clips": [{"asset": {"type": "video", "src": video_url}, "start": 0}]}]}
       |                                                                                                                         ^^^^^^^^^^^^
   397 |             return await _submit_and_maybe_wait(context, {"timeline": timeline, "output": output}, wait)
   398 |         except Exception as e:
       |
   
   E501 Line too long (125 > 120)
      --> shotstack/shotstack.py:453:121
       |
   451 | …         length = duration
   452 | …     else:
   453 | …         return ActionResult(data={"result": False, "error": "Either end_time or duration is required"}, cost_usd=0.0)
       |                                                                                                                   ^^^^^
   454 | …     timeline = {"tracks": [{"clips": [{"asset": {"type": "video", "src": video_url, "trim": start_time}, "start": 0, "length": leng…
   455 | …     return await _submit_and_maybe_wait(context, {"timeline": timeline, "output": output}, wait)
       |
   
   E501 Line too long (146 > 120)
      --> shotstack/shotstack.py:454:121
       |
   452 | …
   453 | … "Either end_time or duration is required"}, cost_usd=0.0)
   454 | …ideo", "src": video_url, "trim": start_time}, "start": 0, "length": length}]}]}
       |                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^
   455 | …": timeline, "output": output}, wait)
   456 | …
       |
   
   E501 Line too long (139 > 120)
      --> shotstack/shotstack.py:522:121
       |
   520 | …
   521 | …
   522 | …r": "Either subtitle_url or auto_generate=True is required"}, cost_usd=0.0)
       |                                                          ^^^^^^^^^^^^^^^^^^^
   523 | …
   524 | …
       |
   
   Found 35 errors.
   ❌ Lint errors found

   Fix: Run 'ruff check --fix' to auto-fix some issues

🎨 Checking formatting with ruff...
   ❌ Formatting issues found

   Fix: Run 'ruff format' to auto-format

🔒 Scanning for security issues with bandit...
   ✅ Security OK

🔗 Checking config-code sync...
   No config.json found in shotstack
   ❌ Config and code are out of sync

   Fix: Ensure config.json actions and input_schema match the code

🔄 Checking fetch patterns...
   ✅ Fetch patterns OK

========================================
❌ CODE CHECK FAILED
========================================
⚠️ Tests output
⚠️  No unit tests (test_*_unit.py) found in: shotstack

Integration    Tests  Coverage        Status
--------------------------------------------
salesforce     56/56       99%      ✅ Passed
--------------------------------------------
Total          56/56            ✅ All passed

✅ Tests passed: salesforce
✅ README Check output
========================================
✅ README CHECK PASSED
========================================
⚠️ Version Check output
✅ salesforce: New integration with version 1.0.0
⚠️ No config.json in shotstack — skipping version check

========================================
✅ VERSION CHECK PASSED
========================================

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04e818f204

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread shotstack/shotstack.py
Comment on lines +10 to +11
config_path = os.path.join(os.path.dirname(__file__), "config.json")
shotstack = Integration.load(config_path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add Shotstack config before loading integration

Integration.load(config_path) is executed at import time, but this commit does not add shotstack/config.json, so importing shotstack/shotstack.py will fail immediately (for example with file-not-found) before any action can run. This makes the newly added Shotstack module non-functional unless the config file is added (or loading is deferred/guarded).

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant