From 29406d73c9e4c56b4d7d69bf0293359b3fc33bde Mon Sep 17 00:00:00 2001 From: Kishan Parmar <135701940+kishanprmr@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:37:42 +0530 Subject: [PATCH 1/2] fix(jira-cloud): stop updated-issue trigger permanently skipping updates (#14921) --- bun.lock | 4 +- .../pieces/community/jira-cloud/package.json | 2 +- .../jira-cloud/src/lib/common/index.ts | 47 ++-- .../jira-cloud/src/lib/common/polling.ts | 233 ++++++++++++++++++ .../src/lib/triggers/updated-issue.ts | 42 +--- 5 files changed, 267 insertions(+), 61 deletions(-) create mode 100644 packages/pieces/community/jira-cloud/src/lib/common/polling.ts diff --git a/bun.lock b/bun.lock index 2f26e9ac1bcf..e1b7ded2424a 100644 --- a/bun.lock +++ b/bun.lock @@ -3677,7 +3677,7 @@ }, "packages/pieces/community/gmail": { "name": "@activepieces/piece-gmail", - "version": "0.12.10", + "version": "0.13.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", @@ -4608,7 +4608,7 @@ }, "packages/pieces/community/jira-cloud": { "name": "@activepieces/piece-jira-cloud", - "version": "0.3.10", + "version": "0.3.11", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", diff --git a/packages/pieces/community/jira-cloud/package.json b/packages/pieces/community/jira-cloud/package.json index 7c30cfd75683..2f94287bd4ea 100644 --- a/packages/pieces/community/jira-cloud/package.json +++ b/packages/pieces/community/jira-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-jira-cloud", - "version": "0.3.10", + "version": "0.3.11", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/jira-cloud/src/lib/common/index.ts b/packages/pieces/community/jira-cloud/src/lib/common/index.ts index 2be26530620d..14477c5a1c13 100644 --- a/packages/pieces/community/jira-cloud/src/lib/common/index.ts +++ b/packages/pieces/community/jira-cloud/src/lib/common/index.ts @@ -70,6 +70,29 @@ export async function getPriorities({ auth }: { auth: JiraAuth }) { return response.body as any[]; } +export async function sanitizeJqlQuery({ auth, jql }: { auth: JiraAuth; jql: string }): Promise { + const sanitizeResult = ( + await sendJiraRequest({ + auth: auth, + url: 'jql/sanitize', + method: HttpMethod.POST, + body: { + queries: [ + { + query: jql, + }, + ], + }, + }) + ).body as { + queries: { + initialQuery: string; + sanitizedQuery: string; + }[]; + }; + return sanitizeResult.queries[0].sanitizedQuery; +} + export async function executeJql({ auth, jql, @@ -87,29 +110,7 @@ export async function executeJql({ queryParams?: QueryParams; body?: HttpMessageBody; }) { - let reqJql = jql; - if (sanitizeJql) { - const sanitizeResult = ( - await sendJiraRequest({ - auth: auth, - url: 'jql/sanitize', - method: HttpMethod.POST, - body: { - queries: [ - { - query: jql, - }, - ], - }, - }) - ).body as { - queries: { - initialQuery: string; - sanitizedQuery: string; - }[]; - }; - reqJql = sanitizeResult.queries[0].sanitizedQuery; - } + const reqJql = sanitizeJql ? await sanitizeJqlQuery({ auth, jql }) : jql; const response = await sendJiraRequest({ auth, diff --git a/packages/pieces/community/jira-cloud/src/lib/common/polling.ts b/packages/pieces/community/jira-cloud/src/lib/common/polling.ts new file mode 100644 index 000000000000..26e67d68e9a9 --- /dev/null +++ b/packages/pieces/community/jira-cloud/src/lib/common/polling.ts @@ -0,0 +1,233 @@ +// https://developer.atlassian.com/cloud/jira/platform/search-and-reconcile/ +import { DEDUPE_KEY_PROPERTY, Store, isNil } from '@activepieces/pieces-framework'; +import { JiraAuth } from '../../auth'; +import { sanitizeJqlQuery, searchIssuesByJql } from './index'; +import { JiraSearchResponse } from './types'; + +const LOOKBACK_MS = 15 * 60 * 1000; +const MAX_RESULTS = 1000; +const MAX_PAGES = 10; +const MAX_LEDGER_ENTRIES = 2000; +const TEST_ITEMS_LIMIT = 5; +const STATE_STORE_KEY = 'pollingState'; + +function toRelativeJqlDate({ sinceEpochMS, now }: { sinceEpochMS: number; now: number }): string { + return `-${Math.max(Math.ceil((now - sinceEpochMS) / 60_000) + 1, 1)}m`; +} + +function stripTrailingOrderBy(jql: string): string { + let quote: '"' | "'" | null = null; + let orderByIndex = -1; + + for (let index = 0; index < jql.length && orderByIndex === -1; index++) { + const char = jql[index]; + + if (quote) { + if (char === '\\') { + index += 1; + } else if (char === quote) { + quote = null; + } + continue; + } + + if (char === '"' || char === "'") { + quote = char; + continue; + } + + if (/\s/.test(char) && /^\s+order\s+by\b/i.test(jql.slice(index))) { + orderByIndex = index; + } + } + + return (orderByIndex === -1 ? jql : jql.slice(0, orderByIndex)).trim(); +} + +async function composeJql({ + auth, + propsValue, + since, + direction, +}: { + auth: JiraAuth; + propsValue: JiraPollingContext['propsValue']; + since: string | null; + direction: 'ASC' | 'DESC'; +}): Promise { + const userJql = isNil(propsValue.jql) ? '' : stripTrailingOrderBy(propsValue.jql); + const scope = + userJql.length === 0 + ? null + : `(${propsValue.sanitizeJql ? await sanitizeJqlQuery({ auth, jql: userJql }) : userJql})`; + const conditions = [scope, isNil(since) ? null : `updated > '${since}'`].filter( + (condition): condition is string => !isNil(condition), + ); + const orderBy = `ORDER BY updated ${direction}`; + return conditions.length === 0 ? orderBy : `${conditions.join(' AND ')} ${orderBy}`; +} + +async function fetchIssues({ + auth, + jql, + maxResults, + maxPages = 1, +}: { + auth: JiraAuth; + jql: string; + maxResults: number; + maxPages?: number; +}): Promise<{ items: PollingItem[]; truncated: boolean }> { + const issues: JiraSearchResponse['issues'] = []; + let nextPageToken: string | undefined; + let truncated = false; + + for (let page = 0; page < maxPages; page++) { + const response: JiraSearchResponse = await searchIssuesByJql({ + auth, + jql, + maxResults, + sanitizeJql: false, + nextPageToken, + }); + issues.push(...(response.issues ?? [])); + nextPageToken = response.nextPageToken; + + if (isNil(nextPageToken)) { + break; + } + if (page === maxPages - 1) { + truncated = true; + } + } + + return { items: toItems(issues), truncated }; +} + +function toItems(issues: JiraSearchResponse['issues']): PollingItem[] { + return issues + .flatMap((issue) => { + const epochMilliSeconds = Date.parse(issue?.fields?.updated); + return Number.isFinite(epochMilliSeconds) + ? [{ key: `${issue?.id}:${epochMilliSeconds}`, epochMilliSeconds, data: issue }] + : []; + }) + .sort((first, second) => first.epochMilliSeconds - second.epochMilliSeconds); +} + +function pruneState({ + entries, + floor, + windowStart, +}: { + entries: LedgerEntry[]; + floor: number; + windowStart: number; +}): PollingState { + const insideWindow = entries + .filter(([, epoch]) => epoch > windowStart) + .sort((first, second) => first[1] - second[1]); + const overflow = Math.max(insideWindow.length - MAX_LEDGER_ENTRIES, 0); + const dropped = [ + ...entries.filter(([, epoch]) => epoch <= windowStart), + ...insideWindow.slice(0, overflow), + ]; + + return { + windowStart, + floor: dropped.reduce( + (highest, [, epoch]) => Math.max(highest, epoch), + Math.max(floor, windowStart), + ), + entries: insideWindow.slice(overflow), + }; +} + +function initialState(now: number): PollingState { + return { windowStart: now, floor: now, entries: [] }; +} + +export const jiraPolling = { + async onEnable({ context }: { context: JiraPollingContext }): Promise { + const { store, isRepublish } = context; + if (isRepublish && !isNil(await store.get(STATE_STORE_KEY))) { + return; + } + await store.put(STATE_STORE_KEY, initialState(Date.now())); + }, + + async poll({ context }: { context: JiraPollingContext }): Promise { + const { auth, propsValue, store } = context; + const now = Date.now(); + const state = (await store.get(STATE_STORE_KEY)) ?? initialState(now); + const windowStart = Math.max(state.windowStart, state.floor); + + const { items, truncated } = await fetchIssues({ + auth, + jql: await composeJql({ + auth, + propsValue, + since: toRelativeJqlDate({ sinceEpochMS: windowStart, now }), + direction: 'ASC', + }), + maxResults: MAX_RESULTS, + maxPages: MAX_PAGES, + }); + + const alreadyEmitted = new Set(state.entries.map(([key]) => key)); + const freshItems = items.filter( + (item) => item.epochMilliSeconds > windowStart && !alreadyEmitted.has(item.key), + ); + + const highestEpoch = items.at(-1)?.epochMilliSeconds; + const nextWindowStart = truncated + ? Math.max(windowStart, isNil(highestEpoch) ? windowStart : highestEpoch - 1) + : Math.max(windowStart, now - LOOKBACK_MS); + + await store.put( + STATE_STORE_KEY, + pruneState({ + entries: [ + ...state.entries, + ...freshItems.map((item): LedgerEntry => [item.key, item.epochMilliSeconds]), + ], + floor: state.floor, + windowStart: nextWindowStart, + }), + ); + + return freshItems.map((item) => ({ ...item.data, [DEDUPE_KEY_PROPERTY]: item.key })); + }, + + async test({ context }: { context: JiraPollingContext }): Promise { + const { auth, propsValue } = context; + const { items } = await fetchIssues({ + auth, + jql: await composeJql({ auth, propsValue, since: null, direction: 'DESC' }), + maxResults: TEST_ITEMS_LIMIT, + }); + + return items.reverse().map((item) => item.data); + }, +}; + +type JiraPollingContext = { + auth: JiraAuth; + propsValue: { jql?: string; sanitizeJql?: boolean }; + store: Store; + isRepublish?: boolean; +}; + +type PollingItem = { + key: string; + epochMilliSeconds: number; + data: Record; +}; + +type LedgerEntry = [key: string, epochMilliSeconds: number]; + +type PollingState = { + windowStart: number; + floor: number; + entries: LedgerEntry[]; +}; diff --git a/packages/pieces/community/jira-cloud/src/lib/triggers/updated-issue.ts b/packages/pieces/community/jira-cloud/src/lib/triggers/updated-issue.ts index 7d46af0c3dbc..4bd0bf1e7bcd 100644 --- a/packages/pieces/community/jira-cloud/src/lib/triggers/updated-issue.ts +++ b/packages/pieces/community/jira-cloud/src/lib/triggers/updated-issue.ts @@ -3,36 +3,8 @@ import { TriggerStrategy, createTrigger, } from '@activepieces/pieces-framework'; -import { - Polling, - DedupeStrategy, - pollingHelper, -} from '@activepieces/pieces-common'; -import { JiraAuth, jiraCloudAuth } from '../../auth'; -import { searchIssuesByJql } from '../common'; -import dayjs from 'dayjs'; - -const polling: Polling< JiraAuth, - { jql?: string; sanitizeJql?: boolean } -> = { - strategy: DedupeStrategy.TIMEBASED, - items: async ({ auth, lastFetchEpochMS, propsValue }) => { - const { jql, sanitizeJql } = propsValue; - const searchQuery = `${jql ? jql + ' AND ' : ''}updated > '${dayjs( - lastFetchEpochMS - ).format('YYYY-MM-DD HH:mm')}'`; - const response = await searchIssuesByJql({ - auth, - jql: searchQuery, - maxResults: 50, - sanitizeJql: sanitizeJql ?? false, - }); - return response.issues.map((issue: any) => ({ - epochMilliSeconds: Date.parse(issue.fields.updated), - data: issue, - })); - }, -}; +import { jiraCloudAuth } from '../../auth'; +import { jiraPolling } from '../common/polling'; export const updatedIssue = createTrigger({ name: 'updated_issue', @@ -58,15 +30,15 @@ export const updatedIssue = createTrigger({ }, sampleData: {}, async onEnable(context) { - await pollingHelper.onEnable(polling, context); + await jiraPolling.onEnable({ context }); }, - async onDisable(context) { - await pollingHelper.onDisable(polling, context); + async onDisable() { + return; }, async run(context) { - return await pollingHelper.poll(polling, context); + return await jiraPolling.poll({ context }); }, async test(context) { - return await pollingHelper.test(polling, context); + return await jiraPolling.test({ context }); }, }); From 71dd1758dc1b04a1ec0349ec23d2424d5055ae05 Mon Sep 17 00:00:00 2001 From: Hazem Adel Date: Thu, 20 Aug 2026 13:00:40 +0300 Subject: [PATCH 2/2] fix(builder): the builder no longer crashes while an agent step is running (#14947) --- packages/web/src/features/agents/agent-timeline/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/features/agents/agent-timeline/index.tsx b/packages/web/src/features/agents/agent-timeline/index.tsx index 87c58e352647..cf75cadd9769 100644 --- a/packages/web/src/features/agents/agent-timeline/index.tsx +++ b/packages/web/src/features/agents/agent-timeline/index.tsx @@ -27,7 +27,7 @@ export const AgentTimeline = ({ agentResult, className = '', }: AgentTimelineProps) => { - if (isNil(agentResult)) { + if (isNil(agentResult) || isNil(agentResult.steps)) { return

{t('No agent output available')}

; } @@ -37,7 +37,7 @@ export const AgentTimeline = ({
- {agentResult.prompt.length > 0 && ( + {(agentResult.prompt?.length ?? 0) > 0 && ( )}