From 027abf25536c589dfdd57334491e942096044754 Mon Sep 17 00:00:00 2001 From: Sergio Serrano Date: Tue, 7 Jul 2026 14:44:06 -0300 Subject: [PATCH 1/4] fix(ci): compute smoke-test matrix in changes job The `matrix` context is not available in a job-level `if:`, so the previous condition (`matrix.template == 'ai-sdk' && ...`) made the whole workflow invalid. Emit a JSON template list from the changes job and build the matrix via fromJSON, guarding on a non-empty list. Preserves the per-template gating: core change tests all templates, otherwise only the changed ones. --- .github/workflows/ci.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 772203e..0cd8dbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: ai_sdk: ${{ steps.filter.outputs.ai_sdk }} mastra: ${{ steps.filter.outputs.mastra }} langchain: ${{ steps.filter.outputs.langchain }} + # JSON array of templates to smoke test. The `matrix` context is not + # available in a job-level `if:`, so we compute the matrix here instead + # of gating individual matrix legs downstream. + templates: ${{ steps.templates.outputs.list }} steps: - uses: actions/checkout@v4 - name: Detect changed paths @@ -42,6 +46,19 @@ jobs: - "templates/mastra/**" langchain: - "templates/langchain/**" + - name: Compute template matrix + id: templates + run: | + if [ "${{ steps.filter.outputs.core }}" = "true" ]; then + echo 'list=["ai-sdk","mastra","langchain"]' >> "$GITHUB_OUTPUT" + else + items=() + [ "${{ steps.filter.outputs.ai_sdk }}" = "true" ] && items+=('"ai-sdk"') + [ "${{ steps.filter.outputs.mastra }}" = "true" ] && items+=('"mastra"') + [ "${{ steps.filter.outputs.langchain }}" = "true" ] && items+=('"langchain"') + joined=$(IFS=,; echo "${items[*]}") + echo "list=[$joined]" >> "$GITHUB_OUTPUT" + fi lint-and-build: name: Lint, Format & Build @@ -88,7 +105,7 @@ jobs: name: Smoke Test — ${{ matrix.template }} runs-on: ubuntu-latest needs: [changes, lint-and-build] - if: ${{ needs.changes.outputs.core == 'true' || (matrix.template == 'ai-sdk' && needs.changes.outputs.ai_sdk == 'true') || (matrix.template == 'mastra' && needs.changes.outputs.mastra == 'true') || (matrix.template == 'langchain' && needs.changes.outputs.langchain == 'true') }} + if: ${{ needs.changes.outputs.templates != '[]' }} env: NPM_CONFIG_AUDIT: "false" NPM_CONFIG_FUND: "false" @@ -97,7 +114,7 @@ jobs: strategy: fail-fast: false matrix: - template: [ai-sdk, mastra, langchain] + template: ${{ fromJSON(needs.changes.outputs.templates) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From f26ca29b44ec93fa08afc6df3016177c03cbd03e Mon Sep 17 00:00:00 2001 From: Sergio Serrano Date: Tue, 7 Jul 2026 14:47:55 -0300 Subject: [PATCH 2/4] fix(ci): use bun instead of npm ci (repo has bun.lock, no package-lock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup-node cache:npm and npm ci both require a package-lock.json, which this bun-based repo doesn't have — CI failed with 'Dependencies lock file is not found'. Install with bun (matching release.yml), and make setup-bun unconditional in smoke-test so the langchain leg can install root deps to run the built CLI. --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cd8dbc..daa0dbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,8 +74,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 - cache: npm - - run: npm ci + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile - run: npm run lint - run: npm run format:check - run: npm run typecheck @@ -121,7 +121,6 @@ jobs: with: node-version: 22 - uses: oven-sh/setup-bun@v2 - if: matrix.template != 'langchain' with: bun-version: latest - name: Cache bun packages @@ -139,7 +138,7 @@ jobs: python-version: "3.12" cache: pip cache-dependency-path: templates/langchain/requirements.txt - - run: npm ci + - run: bun install --frozen-lockfile - name: Download built CLI artifact uses: actions/download-artifact@v4 with: From a4c396608a4132a04870117643c3817da1925d02 Mon Sep 17 00:00:00 2001 From: Sergio Serrano Date: Tue, 7 Jul 2026 15:16:49 -0300 Subject: [PATCH 3/4] fix(ci): green the smoke tests and python lint - Format langchain template with ruff (main.py, routes/arcade.py, routes/plan.py) so 'ruff format --check' passes. - Feed doctor dummy CI env (gateway + LLM key) so its success path runs; it otherwise exits 1 on an unconfigured scaffold. No template changes. --- .github/workflows/ci.yml | 10 ++++++++++ templates/langchain/app/main.py | 1 - templates/langchain/app/routes/arcade.py | 9 ++++----- templates/langchain/app/routes/plan.py | 18 ++++++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daa0dbe..1425483 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,14 +152,24 @@ jobs: working-directory: ci-test-${{ matrix.template }} run: cp .env.example .env + # doctor validates runtime config (gateway URL + LLM key) and pings the + # gateway, so it exits 1 on an unconfigured scaffold. Feed it dummy CI + # values so we exercise its success path. example.com is reliably + # reachable and satisfies the reachability check (returns 200). - name: Doctor (Next.js + TypeScript templates) if: matrix.template != 'langchain' working-directory: ci-test-${{ matrix.template }} + env: + ARCADE_GATEWAY_URL: https://example.com + OPENAI_API_KEY: sk-ci-smoke-test run: bun run doctor - name: Doctor (Python template) if: matrix.template == 'langchain' working-directory: ci-test-${{ matrix.template }} + env: + ARCADE_GATEWAY_URL: https://example.com + OPENAI_API_KEY: sk-ci-smoke-test run: python -m app.doctor - name: Type check (Next.js + TypeScript templates) diff --git a/templates/langchain/app/main.py b/templates/langchain/app/main.py index abe801c..6564b07 100644 --- a/templates/langchain/app/main.py +++ b/templates/langchain/app/main.py @@ -72,4 +72,3 @@ async def dashboard_page(request: Request, db: AsyncSession = Depends(get_db)): if not user: return RedirectResponse("/") return templates.TemplateResponse(request, "dashboard.html", {"user": user}) - diff --git a/templates/langchain/app/routes/arcade.py b/templates/langchain/app/routes/arcade.py index 8884228..2202de6 100644 --- a/templates/langchain/app/routes/arcade.py +++ b/templates/langchain/app/routes/arcade.py @@ -79,8 +79,7 @@ async def connect(request: Request, db: AsyncSession = Depends(get_db)): { "connected": False, "error": ( - "Cannot reach Arcade Gateway. " - "Check ARCADE_GATEWAY_URL and try again." + "Cannot reach Arcade Gateway. Check ARCADE_GATEWAY_URL and try again." ), }, status_code=502, @@ -123,9 +122,9 @@ def _extract_auth_url_from_result(content: str) -> str | None: """Check if a tool result contains an Arcade authorization URL.""" try: parsed = json.loads(content) - url = parsed.get("authorization_url") or ( - parsed.get("structuredContent") or {} - ).get("authorization_url") + url = parsed.get("authorization_url") or (parsed.get("structuredContent") or {}).get( + "authorization_url" + ) if url: return url except (json.JSONDecodeError, AttributeError): diff --git a/templates/langchain/app/routes/plan.py b/templates/langchain/app/routes/plan.py index ef92375..069c3a4 100644 --- a/templates/langchain/app/routes/plan.py +++ b/templates/langchain/app/routes/plan.py @@ -110,15 +110,15 @@ def _build_plan_prompt(): "```\n\n" "URL RULES:\n" "Prefer a direct deep link to the item itself:\n" - "- Slack: use the \"permalink\" field if present" + '- Slack: use the "permalink" field if present' " (https://.slack.com/archives//p)\n" "- GitHub: use the issue or PR URL on github.com\n" "- Linear: use the Linear issue URL\n" "- Gmail: use the Gmail thread URL (https://mail.google.com/mail/u/0/#inbox/)\n" - "- Google Calendar: use the \"htmlLink\" field if present\n" + '- Google Calendar: use the "htmlLink" field if present\n' "If no direct deep link is available, fall back to the most relevant URL" " found anywhere in the tool response.\n" - "Only omit \"url\" if there is truly no URL available in the response at all.\n\n" + 'Only omit "url" if there is truly no URL available in the response at all.\n\n' "Rules:\n" "- One json:task block per ACTIONABLE item (skip empty results, metadata, and errors)\n" "- Brief status text between blocks is fine\n" @@ -216,7 +216,8 @@ async def stream(): # Uses pattern matching so it works regardless of Arcade's exact naming # convention (underscores, dots, or mixed casing are all handled). _KNOWN_SERVICE = re.compile( - r"^(github|gmail|google|calendar|linear|slack)", re.IGNORECASE, + r"^(github|gmail|google|calendar|linear|slack)", + re.IGNORECASE, ) _MUTATION = re.compile( r"create|update|delete|send|reply|post|archive|remove" @@ -228,6 +229,7 @@ def _is_triage_tool(name: str) -> bool: if not _KNOWN_SERVICE.search(name): return False return not _MUTATION.search(name) + MAX_TOOL_RESULT_CHARS = 4000 def _wrap_tool(tool): @@ -246,10 +248,13 @@ async def _truncated_ainvoke(*args, **kwargs): updates = {"coroutine": _truncated_ainvoke} if original_func is not None: + def _truncated_invoke(*args, **kwargs): result = original_func(*args, **kwargs) - s = result if isinstance(result, str) else ( - json.dumps(result) if result else "" + s = ( + result + if isinstance(result, str) + else (json.dumps(result) if result else "") ) if len(s) > MAX_TOOL_RESULT_CHARS: return ( @@ -257,6 +262,7 @@ def _truncated_invoke(*args, **kwargs): + f"\n...[truncated {len(s) - MAX_TOOL_RESULT_CHARS} chars]" ) return result + updates["func"] = _truncated_invoke return tool.model_copy(update=updates) From cc9f56cbf509f5ffe5988dde380d18a03639fd4a Mon Sep 17 00:00:00 2001 From: Sergio Serrano Date: Tue, 7 Jul 2026 15:21:03 -0300 Subject: [PATCH 4/4] fix(ci): install python requirements before langchain smoke doctor The langchain smoke leg ran 'python -m app.doctor' without installing the scaffolded project's requirements, so it failed on 'ModuleNotFoundError: httpx'. Install requirements.txt after scaffolding. --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1425483..fa3bf3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,11 @@ jobs: working-directory: ci-test-${{ matrix.template }} run: cp .env.example .env + - name: Install Python deps (Python template) + if: matrix.template == 'langchain' + working-directory: ci-test-${{ matrix.template }} + run: pip install -r requirements.txt + # doctor validates runtime config (gateway URL + LLM key) and pings the # gateway, so it exits 1 on an unconfigured scaffold. Feed it dummy CI # values so we exercise its success path. example.com is reliably