[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$ffp2ezT86T09pbUiG2tsXTeAjszI6_4rZaZamRE5sDzU":3},{"data":4,"related":99},{"id":5,"title":6,"slug":7,"excerpt":8,"cover":9,"category":11,"author":14,"track":20,"level":23,"reading_minutes":26,"is_featured":27,"published_at":28,"updated_at":28,"body":29,"toc":30,"tags":72,"faqs":86,"related_services":93,"comments_enabled":97,"meta_title":19,"meta_description":98,"canonical_url":19,"noindex":27,"is_live":97},7,"چرا همگام‌سازی JQL در Jira فقط ۵۰ ایشو برمی‌گرداند؟","jira-jql-search-nextpagetoken-pagination","اگر job همگام‌سازی Jira شما بدون خطا تمام می‌شود ولی فقط ۵۰ ایشو را می‌آورد یا در حلقه بی‌پایان گیر می‌کند، مشکل از JQL نیست؛ اندپوینت search به pagination مبتنی بر nextPageToken منتقل شده و startAt و total دیگر وجود ندارند.",{"url":10,"alt":6},"https:\u002F\u002Fapi.cloudicap.com\u002Fmedia\u002Fposts\u002Facfa9ea0-bda9-4ade-a1c3-0f1677851587.webp",{"name":12,"slug":13},"CI\u002FCD و اتوماسیون","cicd",{"name":15,"slug":16,"role":17,"bio":18,"linkedin_url":19,"avatar_url":19},"تحریریه کلودیکپ","cloudicap-editorial","تیم محتوای کلودیکپ","تیم فنی و محتوای کلودیکپ؛ مهندسانی که هر روز با DevOps، Kubernetes و زیرساخت ابری کار می‌کنند و تجربه‌هایشان را اینجا می‌نویسند.",null,{"value":21,"label":22},"engineers","مهندسان",{"value":24,"label":25},"intermediate","متوسط",8,false,"2026-10-08T15:34:25+00:00","\u003Ch2 id=\"شرح-مسئله-job-سبز-است-ولی-داده-نصفه-است-سرویس-جیرا\">شرح مسئله: job سبز است، ولی داده نصفه است - سرویس جیرا\u003C\u002Fh2>\u003Cp>یک job شبانه در پایپ‌لاین داریم که با یک JQL ایشوهای یک پروژه را از \u003Ca href=\"\u002Fservices\u002Fjira-self-hosted\">Jira\u003C\u002Fa> می‌خواند و در دیتابیس داخلی \u003Ccode>upsert\u003C\u002Fcode> می‌کند. روی محیط تست که ۴۰ ایشو دارد همه‌چیز درست است؛ روی محیط production که بیش از ۳۰۰۰ ایشو دارد، job بدون هیچ خطایی تمام می‌شود، لاگ هم \u003Ccode>exit code 0\u003C\u002Fcode> می‌دهد، اما جدول فقط ۵۰ ردیف دارد. نه هشداری، نه خطایی، نه retry‌ای.\u003C\u002Fp>\u003Cp>اگر با اندپوینت قدیمی سرچ کار می‌کنید، خروجی چیزی شبیه این است:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">$ curl -sS -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" \\\n    -G \"$JIRA_BASE\u002Frest\u002Fapi\u002F3\u002Fsearch\" \\\n    --data-urlencode \"jql=project = OPS\" \\\n    --data-urlencode \"maxResults=50\" \\\n  | jq '{startAt, maxResults, total, count: (.issues | length)}'\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cpre>\u003Ccode class=\"language-json\">{\n  \"startAt\": 0,\n  \"maxResults\": 50,\n  \"total\": 3140,\n  \"count\": 50\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>اسکریپت فقط صفحه اول را می‌خواند و چون \u003Ccode>total\u003C\u002Fcode> عدد بزرگی است، در ذهن توسعه‌دهنده یعنی «همه‌چیز اوکی است»، در حالی که \u003Ccode>count\u003C\u002Fcode> عدد ثابت ۵۰ را نشان می‌دهد.\u003C\u002Fp>\u003Ch3 id=\"نشانه-دوم-حلقه‌ای-که-تمام-نمی‌شود\">نشانه دوم: حلقه‌ای که تمام نمی‌شود\u003C\u002Fh3>\u003Cp>وقتی کسی متوجه می‌شود اندپوینت قدیمی deprecated شده، سریع مسیر را به \u003Ccode>\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql\u003C\u002Fcode> عوض می‌کند، اما منطق pagination را دست نمی‌زند و همان \u003Ccode>startAt\u003C\u002Fcode> و \u003Ccode>total\u003C\u002Fcode> را نگه می‌دارد. نتیجه، دو رفتار عجیب است:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">$ curl -sS -o \u002Fdev\u002Fnull -w '%{http_code}\\n' -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" \\\n    -G \"$JIRA_BASE\u002Frest\u002Fapi\u002F3\u002Fsearch\" \\\n    --data-urlencode 'jql=project = OPS'\n410\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>یعنی اندپوینت قدیمی رسماً از دسترس خارج شده و پاسخ ۴۱۰ Gone برمی‌گرداند؛ بدنه پاسخ هم در \u003Ccode>errorMessages\u003C\u002Fcode> توضیح می‌دهد که این API حذف شده است. اما بدتر از ۴۱۰، حالت خاموش است:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">$ curl -sS -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" \\\n    -X POST \"$JIRA_BASE\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql\" \\\n    -H 'Content-Type: application\u002Fjson' \\\n    --data '{\"jql\":\"project = OPS ORDER BY created ASC\",\"startAt\":100,\"maxResults\":50}' \\\n  | jq '{startAt: .startAt, isLast: .isLast, first_keys: [.issues[0:3][].key]}'\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cpre>\u003Ccode class=\"language-json\">{\n  \"startAt\": null,\n  \"isLast\": false,\n  \"first_keys\": [\"OPS-1\", \"OPS-2\", \"OPS-3\"]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>\u003Ccode>startAt\u003C\u002Fcode> در پاسخ وجود ندارد و ورودی‌اش هم نادیده گرفته شده؛ ما دقیقاً همان صفحه اول را با \u003Ccode>OPS-1\u003C\u002Fcode> گرفتیم. حالا حلقه‌ای که منتظر \u003Ccode>total\u003C\u002Fcode> است، یا روی \u003Ccode>null\u003C\u002Fcode> می‌شکند یا تا ابد صفحه اول را می‌خواند:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">page=1 got=50 isLast=false token=eyJvZmZzZXQiOjUw\npage=2 got=50 isLast=false token=eyJvZmZzZXQiOjUw\npage=3 got=50 isLast=false token=eyJvZmZzZXQiOjUw\n...\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>خروجی ثابت است چون پارامتر offset دیگر معنایی ندارد و توکن صفحه هم چون درخواست عوض نشده، همان توکن قبلی است.\u003C\u002Fp>\u003Ch2 id=\"علت-ریشه‌ای-مهاجرت-از-offset-pagination-به-cursor-pagination\">علت ریشه‌ای: مهاجرت از offset pagination به cursor pagination\u003C\u002Fh2>\u003Cp>Jira Cloud اندپوینت‌های \u003Ccode>GET\u002FPOST \u002Frest\u002Fapi\u002F3\u002Fsearch\u003C\u002Fcode> را کنار گذاشته و جایش \u003Ccode>\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql\u003C\u002Fcode> را آورده است. این تغییر فقط تغییر آدرس نیست، مدل صفحه‌بندی عوض شده:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Cth>\u003Cp>مفهوم\u003C\u002Fp>\u003C\u002Fth>\u003Cth>\u003Cp>اندپوینت قدیمی (search)\u003C\u002Fp>\u003C\u002Fth>\u003Cth>\u003Cp>اندپوینت جدید (search\u002Fjql)\u003C\u002Fp>\u003C\u002Fth>\u003C\u002Ftr>\u003Ctr>\u003Ctd>\u003Cp>شروع صفحه\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>\u003Ccode>startAt\u003C\u002Fcode> (offset عددی)\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>\u003Ccode>nextPageToken\u003C\u002Fcode> (رشته مبهم)\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>\u003Cp>اندازه صفحه\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>\u003Ccode>maxResults\u003C\u002Fcode> (پیش‌فرض ۵۰)\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>\u003Ccode>maxResults\u003C\u002Fcode> (پیش‌فرض ۵۰)\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>\u003Cp>تعداد کل\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>\u003Ccode>total\u003C\u002Fcode>\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>وجود ندارد\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>\u003Cp>پایان نتایج\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>مقایسه \u003Ccode>startAt + maxResults\u003C\u002Fcode> با \u003Ccode>total\u003C\u002Fcode>\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>\u003Ccode>isLast\u003C\u002Fcode> یا خالی بودن توکن\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>\u003Cp>پایداری نتیجه\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>ضعیف؛ با ویرایش همزمان، ایشو تکراری یا جاافتاده می‌شود\u003C\u002Fp>\u003C\u002Ftd>\u003Ctd>\u003Cp>بهتر؛ توکن به مکان نتیجه گره خورده است\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>دلیل حذف \u003Ccode>total\u003C\u002Fcode> هم ساده است: محاسبه تعداد کل یک result set بزرگ در Jira گران است و برای اکثر کلاینت‌ها هم بی‌فایده. اما همین حذف، اسکریپت‌هایی را که شرط پایانشان \u003Ccode>startAt &lt; total\u003C\u002Fcode> بود، به دو سرنوشت می‌کشاند: یا فقط صفحه اول را می‌خوانند، یا در حلقه بی‌پایان می‌مانند. نکته مهم این است که JQL شما سالم است؛ باگ در سمت کلاینت و در منطق صفحه‌بندی است.\u003C\u002Fp>\u003Cblockquote>\u003Cp>هر پارامتری که در پاسخ نباشد ولی در کد به‌عنوان شرط حلقه استفاده شود، یک بمب ساعتی است. قانون طلایی: شرط پایان حلقه را از پاسخ API بگیر، نه از حدس خودت.\u003C\u002Fp>\u003C\u002Fblockquote>\u003Ch2 id=\"راه‌حل-گام‌به‌گام\">راه‌حل گام‌به‌گام\u003C\u002Fh2>\u003Ch3 id=\"گام-۱-مطمئن-شو-کدام-اندپوینت-زنده-است\">گام ۱: مطمئن شو کدام اندپوینت زنده است\u003C\u002Fh3>\u003Cp>با یک توکن API و یک پروژه کوچک شروع کن. اگر پاسخ ۴۱۰ گرفتی، اندپوینت قدیمی حذف شده و باید به \u003Ccode>search\u002Fjql\u003C\u002Fcode> بروی.\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">export JIRA_BASE=\"https:\u002F\u002Fyour-domain.atlassian.net\"\nexport JIRA_EMAIL=\"you@example.com\"\nexport JIRA_API_TOKEN=\"...\"   # از id.atlassian.com\u002Fmanage-profile\u002Fsecurity\u002Fapi-tokens\n\ncurl -sS --fail-with-body -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" \\\n  -X POST \"$JIRA_BASE\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql\" \\\n  -H 'Accept: application\u002Fjson' \\\n  -H 'Content-Type: application\u002Fjson' \\\n  --data '{\"jql\":\"project = OPS ORDER BY updated ASC\",\"maxResults\":2,\"fields\":[\"summary\",\"status\",\"updated\"]}' \\\n| jq '{count: (.issues | length), isLast, nextPageToken, keys: [.issues[].key]}'\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>اگر \u003Ccode>nextPageToken\u003C\u002Fcode> و \u003Ccode>isLast\u003C\u002Fcode> را در بدنه پاسخ دیدی، در مسیر درستی هستی. \u003Ccode>key\u003C\u002Fcode> و \u003Ccode>id\u003C\u002Fcode> هر ایشو همیشه در پاسخ هستند و نیازی به درخواستشان در \u003Ccode>fields\u003C\u002Fcode> نیست.\u003C\u002Fp>\u003Ch3 id=\"گام-۲-اسکریپت-همگام‌سازی-با-cursor\">گام ۲: اسکریپت همگام‌سازی با cursor\u003C\u002Fh3>\u003Cp>این اسکریپت تا وقتی \u003Ccode>isLast\u003C\u002Fcode> یا توکن تمام نشود جلو می‌رود و اگر توکن تکرار شد، عمداً fail می‌کند تا حلقه بی‌پایان شکل نگیرد:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">#!\u002Fusr\u002Fbin\u002Fenv bash\nset -euo pipefail\n\n: \"${JIRA_BASE:?JIRA_BASE را ست کن، مثلا https:\u002F\u002Fyour-domain.atlassian.net}\"\n: \"${JIRA_EMAIL:?}\"\n: \"${JIRA_API_TOKEN:?}\"\n\nJQL=\"${JQL:-project = OPS ORDER BY updated ASC}\"\nMAX_RESULTS=\"${MAX_RESULTS:-100}\"\nOUT_DIR=\"${OUT_DIR:-.\u002Fout}\"\nmkdir -p \"$OUT_DIR\"\n: &gt; \"$OUT_DIR\u002Fissues.jsonl\"\n\npage=0\ntoken=\"\"\n\nwhile :; do\n  payload=$(jq -n \\\n    --arg jql \"$JQL\" \\\n    --argjson mr \"$MAX_RESULTS\" \\\n    --arg token \"$token\" \\\n    '{jql: $jql,\n      maxResults: $mr,\n      fields: [\"summary\", \"status\", \"updated\", \"project\"]}\n     + (if $token == \"\" then {} else {nextPageToken: $token} end)')\n\n  resp=$(curl -sS --fail-with-body --retry 5 --retry-delay 2 \\\n    -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" \\\n    -X POST \"$JIRA_BASE\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql\" \\\n    -H 'Accept: application\u002Fjson' \\\n    -H 'Content-Type: application\u002Fjson' \\\n    --data \"$payload\")\n\n  page=$((page + 1))\n  got=$(jq '.issues | length' &lt;&lt;&lt;\"$resp\")\n  jq -c '.issues[]' &lt;&lt;&lt;\"$resp\" &gt;&gt; \"$OUT_DIR\u002Fissues.jsonl\"\n\n  prev_token=\"$token\"\n  token=$(jq -r '.nextPageToken \u002F\u002F empty' &lt;&lt;&lt;\"$resp\")\n  is_last=$(jq -r '.isLast \u002F\u002F false' &lt;&lt;&lt;\"$resp\")\n\n  echo \"page=$page got=$got isLast=$is_last token=${token:0:12}\"\n\n  [ \"$got\" -eq 0 ] &amp;&amp; break\n  [ \"$is_last\" = \"true\" ] &amp;&amp; break\n  [ -z \"$token\" ] &amp;&amp; break\n\n  if [ \"$token\" = \"$prev_token\" ]; then\n    echo \"nextPageToken عوض نشد؛ توقف برای جلوگیری از حلقه بی‌پایان\" &gt;&amp;2\n    exit 1\n  fi\ndone\n\nunique=$(jq -r '.key' \"$OUT_DIR\u002Fissues.jsonl\" | sort -u | wc -l)\necho \"synced_issues=$unique\"\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>چند نکته عملی:\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>ساخت بدنه JSON با \u003Ccode>jq -n\u003C\u002Fcode> انجام می‌شود تا مشکل escape شدن JQL و کوتیشن‌ها نداشته باشیم.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>پارامتر \u003Ccode>nextPageToken\u003C\u002Fcode> فقط وقتی فرستاده می‌شود که مقدار داشته باشد؛ فرستادن رشته خالی رفتار نامشخصی می‌دهد.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>--fail-with-body\u003C\u002Fcode> نیاز به curl نسخه ۷.۷۶ یا بالاتر دارد. برای خطاهای گذرا مثل ۴۲۹ و ۵xx، \u003Ccode>--retry\u003C\u002Fcode> خودش هدر \u003Ccode>Retry-After\u003C\u002Fcode> را در نظر می‌گیرد.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>توکن صفحه‌بندی را هرگز parse یا دستکاری نکن؛ یک رشته مبهم است و ساختارش می‌تواند بدون اطلاع تغییر کند.\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Ch3 id=\"گام-۳-همگام‌سازی-افزایشی\">گام ۳: همگام‌سازی افزایشی\u003C\u002Fh3>\u003Cp>برای job‌های دوره‌ای، کل پروژه را نخوان. یک watermark نگه دار و با یک بازه همپوشان کوچک بخوان تا ایشوهایی که وسط اجرا ویرایش شده‌اند جا نیفتند، سپس بر اساس \u003Ccode>id\u003C\u002Fcode> ایشو \u003Ccode>upsert\u003C\u002Fcode> کن:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-sql\">-- نمونه جدول مقصد\nCREATE TABLE jira_issues (\n  issue_id   text PRIMARY KEY,   -- از فیلد id، نه key\n  issue_key  text NOT NULL,\n  summary    text,\n  status     text,\n  updated_at timestamptz,\n  synced_at  timestamptz NOT NULL DEFAULT now()\n);\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>و JQL را با مرتب‌سازی صریح بفرست، مثل \u003Ccode>project = OPS AND updated &gt;= \"2025-01-01 00:00\" ORDER BY updated ASC\u003C\u002Fcode>. مرتب‌سازی صریح باعث می‌شود صفحه‌بندی cursor قابل پیش‌بینی بماند.\u003C\u002Fp>\u003Ch2 id=\"بررسی-اینکه-مشکل-واقعا-حل-شده-است\">بررسی اینکه مشکل واقعاً حل شده است\u003C\u002Fh2>\u003Cp>اولین کار این است که تعداد واقعی را از یک منبع مستقل بگیری. اندپوینت \u003Ccode>search\u002Fapproximate-count\u003C\u002Fcode> در Jira Cloud دقیقاً برای همین ساخته شده و جای \u003Ccode>total\u003C\u002Fcode> را می‌گیرد (روی Data Center در دسترس نیست):\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">count_jql=\"${JQL% ORDER BY*}\"\n\nexpected=$(curl -sS -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" \\\n  -X POST \"$JIRA_BASE\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fapproximate-count\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  --data \"$(jq -n --arg jql \"$count_jql\" '{jql: $jql}')\" | jq '.count')\n\nsynced=$(jq -r '.key' \"$OUT_DIR\u002Fissues.jsonl\" | sort -u | wc -l)\necho \"expected=$expected synced=$synced\"\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>سپس این چهار بررسی را انجام بده:\u003C\u002Fp>\u003Col>\u003Cli>\u003Cp>\u003Cstrong>تعداد یکتا برابر تعداد مورد انتظار باشد.\u003C\u002Fstrong> اگر \u003Ccode>synced\u003C\u002Fcode> کمتر است، احتمالاً هنوز صفحه‌ای جا می‌افتد؛ اگر بیشتر است، ایشو تکراری در خروجی داری و باید با \u003Ccode>sort -u\u003C\u002Fcode> روی \u003Ccode>id\u003C\u002Fcode> یکتاسازی کنی.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>اجرای دوم job باید تقریباً صفر تغییر ثبت کند.\u003C\u002Fstrong> اگر هر بار همان ایشوها را می‌نویسد، watermark یا کلید یکتای \u003Ccode>upsert\u003C\u002Fcode> ایراد دارد.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>تعداد صفحات را لاگ کن.\u003C\u002Fstrong> برای ۳۰۰۰ ایشو با \u003Ccode>maxResults=100\u003C\u002Fcode> باید حدود ۳۱ صفحه ببینی؛ نه یک صفحه، نه بی‌نهایت.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>خروجی را با UI مقایسه کن.\u003C\u002Fstrong> یک export ساده CSV از همان فیلتر بگیر و تعداد ردیف‌ها را چک کن. اختلاف‌های کوچک معمولاً مربوط به permission خود کاربر API است، نه صفحه‌بندی.\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Fol>\u003Ch2 id=\"پیشگیری-و-بهترین-روش‌ها\">پیشگیری و بهترین روش‌ها\u003C\u002Fh2>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>یک تست قراردادی با دیتای بزرگتر از یک صفحه داشته باش.\u003C\u002Fstrong> یک پروژه تستی با ۱۲۰ ایشو بساز و در CI هفتگی برو آن را بخوان. این تنها راه گرفتن باگ‌های صفحه‌بندی قبل از production است.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>هرگز به نبودن فیلد در پاسخ تکیه نکن.\u003C\u002Fstrong> اگر \u003Ccode>total\u003C\u002Fcode> وجود ندارد، \u003Ccode>jq '.total \u002F\u002F 0'\u003C\u002Fcode> نباید شرط حلقه شود.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>شرط پایان را دوگانه کن.\u003C\u002Fstrong> هم \u003Ccode>isLast\u003C\u002Fcode> را چک کن، هم خالی بودن توکن، هم صفر بودن تعداد نتایج همان صفحه را.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>محافظ حلقه بگذار.\u003C\u002Fstrong> یک سقف مثل «حداکثر ۵۰۰ صفحه» و یک alert روی «تعداد صفحات بیشتر از حد انتظار» عملاً هر باگ صفحه‌بندی را لو می‌دهد.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>فیلدها را صریح بخواه.\u003C\u002Fstrong> درخواست همه فیلدها روی هزاران ایشو، هم کند است هم حجم پاسخ را بی‌دلیل بالا می‌برد.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>برای رخدادهای نزدیک‌به‌واقعی از webhook استفاده کن، ولی idempotent بمان.\u003C\u002Fstrong> webhook ممکن است تکرار شود؛ پس پردازش را بر اساس \u003Ccode>id\u003C\u002Fcode> ایشو و شناسه رویداد یکتاسازی کن.\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>تغییرات API را رصد کن.\u003C\u002Fstrong> این مهاجرت نمونه روشنی از تغییرات شکست‌دهنده در Jira Cloud است؛ نسخه API را در کد pin کن و changelog را دنبال کن.\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Ch2 id=\"پرسش‌های-پرتکرار\">پرسش‌های پرتکرار\u003C\u002Fh2>\u003Ch3 id=\"هنوز-می‌توانم-از-rest-api-3-search-استفاده-کنم\">هنوز می‌توانم از \u003Ccode>\u002Frest\u002Fapi\u002F3\u002Fsearch\u003C\u002Fcode> استفاده کنم؟\u003C\u002Fh3>\u003Cp>روی Jira Cloud این اندپوینت deprecated شده و پاسخ ۴۱۰ Gone می‌دهد، پس چاره‌ای جز مهاجرت به \u003Ccode>\u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql\u003C\u002Fcode> نداری. روی Data Center و Server وضعیت نسخه‌به‌نسخه متفاوت است؛ مستندات نسخه خودت را چک کن و کدی بنویس که با مدل cursor هم کار کند تا مهاجرت بعدی دردناک نباشد.\u003C\u002Fp>\u003Ch3 id=\"حالا-که-total-حذف-شده-چطور-تعداد-کل-ایشوها-را-بگیرم\">حالا که \u003Ccode>total\u003C\u002Fcode> حذف شده، چطور تعداد کل ایشوها را بگیرم؟\u003C\u002Fh3>\u003Cp>در Jira Cloud از \u003Ccode>POST \u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fapproximate-count\u003C\u002Fcode> با همان JQL استفاده کن؛ در پاسخ یک فیلد \u003Ccode>count\u003C\u002Fcode> می‌گیری. به یاد داشته باش که این عدد تقریبی است و برای مقایسه و پایش مناسب است، نه برای شرط پایان حلقه. شرط پایان باید همیشه خود پاسخ صفحه‌بندی باشد.\u003C\u002Fp>\u003Ch3 id=\"چرا-اسکریپت-من-در-حلقه-بی‌پایان-می‌افتد\">چرا اسکریپت من در حلقه بی‌پایان می‌افتد؟\u003C\u002Fh3>\u003Cp>تقریباً همیشه یکی از این دو دلیل است: یا پارامتر \u003Ccode>startAt\u003C\u002Fcode> را به اندپوینت جدید می‌فرستی که نادیده گرفته می‌شود و همیشه صفحه اول را می‌گیری، یا متغیر توکن را در هر تکرار به‌روز نمی‌کنی. راه‌حل این است که در هر تکرار \u003Ccode>nextPageToken\u003C\u002Fcode> را از پاسخ بخوانی، در بدنه درخواست بعدی بگذاری و اگر توکن با توکن قبلی یکی بود، اسکریپت را با خطا متوقف کنی.\u003C\u002Fp>",[31,35,39,42,45,48,51,54,57,60,63,66,69],{"id":32,"text":33,"level":34},"شرح-مسئله-job-سبز-است-ولی-داده-نصفه-است-سرویس-جیرا","شرح مسئله: job سبز است، ولی داده نصفه است - سرویس جیرا",2,{"id":36,"text":37,"level":38},"نشانه-دوم-حلقه‌ای-که-تمام-نمی‌شود","نشانه دوم: حلقه‌ای که تمام نمی‌شود",3,{"id":40,"text":41,"level":34},"علت-ریشه‌ای-مهاجرت-از-offset-pagination-به-cursor-pagination","علت ریشه‌ای: مهاجرت از offset pagination به cursor pagination",{"id":43,"text":44,"level":34},"راه‌حل-گام‌به‌گام","راه‌حل گام‌به‌گام",{"id":46,"text":47,"level":38},"گام-۱-مطمئن-شو-کدام-اندپوینت-زنده-است","گام ۱: مطمئن شو کدام اندپوینت زنده است",{"id":49,"text":50,"level":38},"گام-۲-اسکریپت-همگام‌سازی-با-cursor","گام ۲: اسکریپت همگام‌سازی با cursor",{"id":52,"text":53,"level":38},"گام-۳-همگام‌سازی-افزایشی","گام ۳: همگام‌سازی افزایشی",{"id":55,"text":56,"level":34},"بررسی-اینکه-مشکل-واقعا-حل-شده-است","بررسی اینکه مشکل واقعاً حل شده است",{"id":58,"text":59,"level":34},"پیشگیری-و-بهترین-روش‌ها","پیشگیری و بهترین روش‌ها",{"id":61,"text":62,"level":34},"پرسش‌های-پرتکرار","پرسش‌های پرتکرار",{"id":64,"text":65,"level":38},"هنوز-می‌توانم-از-rest-api-3-search-استفاده-کنم","هنوز می‌توانم از \u002Frest\u002Fapi\u002F3\u002Fsearch استفاده کنم؟",{"id":67,"text":68,"level":38},"حالا-که-total-حذف-شده-چطور-تعداد-کل-ایشوها-را-بگیرم","حالا که total حذف شده، چطور تعداد کل ایشوها را بگیرم؟",{"id":70,"text":71,"level":38},"چرا-اسکریپت-من-در-حلقه-بی‌پایان-می‌افتد","چرا اسکریپت من در حلقه بی‌پایان می‌افتد؟",[73,75,77,80,83],{"name":74,"slug":74},"jira",{"name":76,"slug":13},"CI\u002FCD",{"name":78,"slug":79},"JQL","jql",{"name":81,"slug":82},"REST API","rest-api",{"name":84,"slug":85},"Pagination","pagination",[87,89,91],{"question":65,"answer":88},"روی Jira Cloud این اندپوینت deprecated شده و پاسخ ۴۱۰ Gone برمی‌گرداند، پس باید به \u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fjql مهاجرت کنی. روی Data Center و Server وضعیت نسخه‌به‌نسخه متفاوت است؛ مستندات نسخه خودت را چک کن.",{"question":68,"answer":90},"در Jira Cloud از POST \u002Frest\u002Fapi\u002F3\u002Fsearch\u002Fapproximate-count با همان JQL استفاده کن؛ فیلد count را برمی‌گرداند. این عدد تقریبی است و برای مقایسه و پایش مناسب است، نه برای شرط پایان حلقه.",{"question":71,"answer":92},"یا پارامتر startAt را به اندپوینت جدید می‌فرستی که نادیده گرفته می‌شود و همیشه صفحه اول را می‌گیری، یا متغیر nextPageToken را در هر تکرار به‌روز نمی‌کنی. توکن را از پاسخ بخوان، در درخواست بعدی بگذار و اگر تکرار شد اجرا را با خطا متوقف کن.",[94,95,96],"jira-self-hosted","cloud-migration","devops-consulting",true,"چرا اسکریپت همگام‌سازی Jira فقط ۵۰ ایشو می‌آورد یا در حلقه بی‌پایان می‌افتد؟ مهاجرت از startAt و total به nextPageToken در search\u002Fjql و راه‌حل کامل و قابل اجرا.",[100,112,127],{"id":101,"title":102,"slug":103,"excerpt":104,"cover":105,"category":107,"author":108,"track":109,"level":110,"reading_minutes":5,"is_featured":27,"published_at":111,"updated_at":111},4,"حلقه بی‌پایان Jira Automation: وقتی قانون خودش را صدا می‌زند","jira-automation-infinite-loop-fix","اگر trigger و action یک قانون Jira Automation روی یک رویداد بیفتند، قانون خودش را دوباره اجرا می‌کند و issue با صدها کامنت تکراری پر می‌شود. این مقاله علت، پاک‌سازی و راه‌حل پایدار را نشان می‌دهد.",{"url":106,"alt":102},"https:\u002F\u002Fapi.cloudicap.com\u002Fmedia\u002Fposts\u002F696958ce-e1fa-458e-b7df-1f1a99fe6a5f.webp",{"name":12,"slug":13},{"name":15,"slug":16,"role":17,"avatar_url":19},{"value":21,"label":22},{"value":24,"label":25},"2026-10-07T09:28:33+00:00",{"id":113,"title":114,"slug":115,"excerpt":116,"cover":117,"category":119,"author":122,"track":123,"level":124,"reading_minutes":125,"is_featured":27,"published_at":126,"updated_at":126},5,"قفل state ترافورم گیر کرده؛ آزادسازی امن بعد از کیل شدن apply","terraform-state-lock-stuck-force-unlock","وقتی پایپلاین CI وسط اجرای apply کشته می‌شود، قفل state ترافورم در جدول DynamoDB باقی می‌ماند و اجرای بعدی با خطای Error acquiring the state lock متوقف می‌شود. در این مقاله علت ریشه‌ای، روش آزادسازی امن و راه‌های پیشگیری را بررسی می‌کنیم.",{"url":118,"alt":114},"https:\u002F\u002Fapi.cloudicap.com\u002Fmedia\u002Fposts\u002Fe26789f7-eced-4abb-b6a9-a91adf9b7500.webp",{"name":120,"slug":121},"زیرساخت ابری و IaC","cloud-infrastructure",{"name":15,"slug":16,"role":17,"avatar_url":19},{"value":21,"label":22},{"value":24,"label":25},6,"2026-10-08T09:49:26+00:00",{"id":38,"title":128,"slug":129,"excerpt":130,"cover":131,"category":133,"author":136,"track":137,"level":138,"reading_minutes":5,"is_featured":27,"published_at":139,"updated_at":139},"قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل","haproxy-websocket-timeout-50s","اگر WebSocket یا SSE پشت HAProxy هر ۵۰ ثانیه قطع می‌شود، مقصر تایماوت‌های پیش‌فرض فایل نمونه است. در این مقاله علت ریشه‌ای و پیکربندی درست timeout tunnel را بررسی می‌کنیم.",{"url":132,"alt":128},"https:\u002F\u002Fapi.cloudicap.com\u002Fmedia\u002Fposts\u002F26b583b4-d805-45d6-a265-a94d73b5a21d.webp",{"name":134,"slug":135},"Linux و مدیریت سرور","linux",{"name":15,"slug":16,"role":17,"avatar_url":19},{"value":21,"label":22},{"value":24,"label":25},"2026-10-07T07:57:29+00:00"]