دیپلوی وردپرس بدون شکستن سایت؛ اجرای آزمایشی روی کپی، بازگشت دقیق و تأیید با نشانه

امید بداق عمومی بدون دیدگاه ۲۴ دقیقه مطالعه

این مقاله عضو خوشهٔ «مهاجرت، دیپلوی و پایش» است. موضوع را از یک حالت خاص شروع می‌کنم، نه از تعریف عمومی: روی هاست اشتراکی، بیشتر خرابی‌هایی که ساعت‌بر بودند، از آپدیت هسته یا افزونهٔ خراب نیامده‌اند؛ از یک خط فرمان بازنویسی متن، در جا، روی فایل زنده آمده‌اند. سریع‌ترین کاری که می‌شود کرد، و تنها کاری که می‌تواند سایت را به حالتی ببرد که نه خطا می‌دهد و نه درست کار می‌کند.

تصویر مشخص است: یک رشته در چند فایل قالب تکرار شده و باید عوض شود. به‌جای ساختن بستهٔ تازه و جایگزینی فایل، یک دستور متنی روی همان مسیر زنده اجرا می‌شود. تمام هم تمام شد؛ پوشه باز است، سایت بالا است، و هیچ چیزی قرمز نشده. دو روز بعد مشخص می‌شود الگوی جایگزینی، متنی را که نباید بخورد، خورده است.

اگر همین کار را بارها انجام داده باشی، مشکل تو مهارت نیست؛ نبودِ سه چیز است: یک اجرای آزمایشی روی کپی، یک مسیر ثبت‌شده برای بازگشت، و یک نشانه که ثابت کند کد تازه واقعاً اجرا شده. این مقاله همان سه چیز است، به‌همراه ترتیب اجرا و شرط توقف در هر مرحله.

دیپلوی روی هاست اشتراکی دقیقاً یعنی چه

دیپلوی یعنی مجموعه‌ای از تغییرات که با هم معنا دارند و باید با هم وارد محیط اجرا شوند: چند فایل قالب یا افزونه، گاهی یک ردیف در جدول گزینه‌ها، و گاهی یک تغییر در فایل تنظیمات. تعریف مهم‌تر از ابزار است؛ چون روی سی‌پنلِ یک هاست اشتراکی، معمولاً هیچ‌کدام از آن لایه‌های ایمنی که در یک محیط کاری بزرگ وجود دارد، در دسترس نیست. نه مخزن نسخه، نه خط تولید خودکار، نه محیط میانی که بتوانی قبل از زنده دیدنش چیزی را در آن بشکنی.

سه نوع تغییر، سه سطح ریسک:

فایل تازه گذاشتن. کم‌ریسک‌ترین. اگر اشتباه باشد، فایل را برمی‌داری و اثری نمی‌ماند؛ چون هیچ چیز قبلی در آن مسیر وجود نداشته.

جایگزینی یک فایل موجود. ریسک متوسط. نسخهٔ قبلی را باید جایی نگه داشته باشی، و بازگشت یعنی همان یک فایل برگردد.

بازنویسی درجا. پرریسک‌ترین. یعنی محتوای میانی یک فایل موجود با دستور متنی عوض شود، بدون اینکه فایل تازه‌ای ساخته شود.

سوال این مقاله فقط دربارهٔ نوع سوم است، چون نوع اول و دوم خودشان راه ایمن دارند. بازنویسی درجا جذب‌کننده است چون کوتاه است: یک رشته در ده فایل تکرار شده و یک فرمان هر ده تا را در چند ثانیه عوض می‌کند. دقیقاً به همین دلیل، دامنهٔ خسارتش هم ده فایل است، نه یکی.

چرا بازنویسی درجا پرخطرترین نوع تغییر است

سه ویژگی این روش، هم‌زمان علیه تو کار می‌کند.

بی‌صدا است. اگر الگو هیچ جایی تطبیق پیدا نکند، بسیاری از این فرمان‌ها با خروجی موفق تمام می‌شوند. یعنی «هیچ اتفاقی نیفتاد» و «همه‌چیز درست انجام شد» از بیرون یک شکل دارند. تا شمار تطبیق‌ها را نخوانی، نمی‌فهمی چیزی عوض شده یا نه.

بازگشت ندارد، مگر از پیش طراحی‌اش کرده باشی. فرمانی که در جا می‌نویسد، نسخهٔ قبلی را نگه نمی‌دارد، مگر خودت صریحاً کپی ساخته باشی. پس اگر نتیجهٔ تغییر را دوست نداشتی، تنها راه برگشتن، بازگردانی از یک نسخهٔ کلی است؛ یعنی همان چیزی که نمی‌خواهی.

دامنه‌اش بزرگ است. یک الگوی دست‌جمعی روی یک پوشه، هر فایلی را که بخورد عوض می‌کند؛ از جمله آن‌هایی که اصلاً در ذهن نداشتی: فایل‌های یک قالب رهاشده، کپی‌های پشتیبانی که کنار فایل اصلی جا مانده‌اند، یا فایلی که در یک زیرپوشهٔ فراموش‌شده خوابیده است.

و سه منبع واقعیِ نادرست تعبیر شدنِ الگو، که هیچ‌کدام خطا نمی‌دهند:

نشانگر متن تطبیق‌شده در بخش جایگزین. در بسیاری از این فرمان‌ها، یک کاراکتر خاص در سمت جایگزین به معنی «همان متنی را بگذار که تطبیق یافت» است. اگر منظورت همان کاراکتر به‌صورت معمولی بوده، باید فرار داده شود؛ اگر نشود، به‌جای یک کاراکتر، کل متنِ خورده‌شده دوباره درج می‌شود و نتیجه طولانی‌تر از خواستهٔ توست، بدون هیچ خطایی.

حریص بودن تطبیق. الگویی که با یک کاراکترِ «هر چیزی» نوشته شود، معمولاً تا آخرین نقطهٔ ممکن در همان سطر پیش می‌رود. یعنی به‌جای یک تکهٔ کوچک، بخش زیادی از سطر خورده می‌شود و اگر بخشی از آن درون یک رشتهٔ متنی باشد، فایل همچنان معتبر می‌ماند.

ارجاع به گروهی که شرکت نکرده. اگر به گروهی در الگو ارجاع دهی که در آن اجرا خالی مانده، خروجی، رشتهٔ خالی است. حذف بی‌صدا، بدون پیام.

یک مورد را جدی بگیر که مخصوص سایت‌های فارسی است: اگر محل اجرای فرمان با تنظیمات رشته‌ایِ نادرست باشد، فرمان می‌تواند متن را بایت‌به‌بایت ببیند. در این حالت یک الگوی ساده می‌تواند از میانهٔ یک کاراکتر چندبایتیِ فارسی رد شود و رشتهٔ داخل فایل را به متن بی‌معنی تبدیل کند؛ فایل از نظر نحوی سالم است، اما نوشتهٔ کاربری خراب شده. بعد از هر بازنویسی، معتبر بودن رشتهمتن فایل را جداگانه بسنج، نه فقط نحوی‌اش را.

بدترین حالت: فایل خراب شده و هیچ خطایی نیست

این همان حالتی است که بیش از همه جدی گرفته‌ام. بعد از بازنویسی، سه حالت ممکن است:

یک: سایت خطا می‌دهد. بدترین خبر نیست؛ خطا آدرس می‌دهد، و تو سریع برمی‌گردی.

دو: فایل خراب شده ولی نحوی‌اش سالم است. سایت بالا می‌ماند و کار نادرست را می‌کند. یک فراوری تودرتو که از اجرای دوبارهٔ همان فرمان آمده، متن را دو بار کدگذاری می‌کند و کاربر رشتهٔ زشتی می‌بیند؛ هیچ‌جا خطا ثبت نشده، و ابزار بررسی نحوی، «سالم است» گزارش می‌دهد. دلیلش ساده است: بررسی نحوی فقط می‌گوید فایل تبدیل به کد می‌شود، نمی‌گوید آن کد همان کاری را می‌کند که تو خواسته‌ای.

سه: اصلاً چیزی عوض نشده، ولی تو فکر می‌کنی عوض شده. این حالت از دو چیز می‌آید: یا الگو هیچ تطبیقی نداشته، یا فایل درست را عوض کرده‌ای و آن فایلی که اجرا می‌شود، جای دیگری است.

حالت دوم و سوم را نمی‌توانی با نگاه کردن به فایل تشخیص بدهی. نگاه کردن به فایل فقط یک سوال را جواب می‌دهد: آیا روی دیسک همان چیزی است که می‌خواستم؟ سوال دوم این است: آیا کد تازه اجرا شد؟ اولی با مقایسهٔ اثر انگشت فایل جواب می‌دهد، دومی فقط با نشانه‌ای که از داخل همان کد تازه تولید شود.

مرحلهٔ یک: اول وضعیت فعلی را ثبت کن

قبل از اینکه چیزی را عوض کنی، چهار چیز را بنویس. این مرحله دو دقیقه وقت می‌گیرد و در روز بد، همان چیزی است که بازگشت دقیق را ممکن می‌کند.

فهرست هدف. فایل‌هایی که قرار است دست بخورند، در یک فایل متنی بیرون از وب. اگر فهرست نداری، دامنهٔ تغییر را کنترل نداری.

اثر انگشت و مجوز همان فایل‌ها. هش هر فایل، به‌همراه مجوز و مالک. مجوز را هم ثبت کن، چون برخی فرمان‌های بازنویسی فایل را از نو می‌سازند و ممکن است ویژگی‌هایش عوض شود.

شمار تطبیق‌های مورد انتظار. قبل از اجرا بشمار الگو چند جا را می‌بیند. اگر انتظار سه مورد داری و شمار، هفده نشان داد، توقف همان‌جاست؛ نه بعد از اعمال. این ارزان‌ترین نقطهٔ توقف در کل مسیر است.

کپی فایل‌های هدف، بیرون از درخت وب. نه کنار خودشان، نه با پسوندهای اتوماتیکِ همان فرمان. دربارهٔ این نکته یک بخش مستقل دارم، چون بیش از هر چیز دیگری در این روش اشتباه می‌شود.

ثبت را در یک پوشهٔ زمان‌دار انجام بده، نه روی پوشهٔ قبلی؛ اگر دو بار در یک ماه تغییر بدهی، دو سابقهٔ جدا می‌خواهی. تاریخ را در نام پوشه بنویس و در همان روز، هر خطی که تغییر می‌دهی را به یک فایل سابقه اضافه کن: مسیر، هش قبل، هش بعد، زمان، و نام کسی که اجرا کرده. این سابقه، نقشهٔ بازگشت توست.

یک نکتهٔ شمارشی که زیاد گم می‌کند: شمارش سطرهای مطابق با شمارش تعداد تطبیق‌ها یکی نیست. یک سطر می‌تواند سه بار الگو را ببیند. اگر برای تشخیص مهم است، هر دو را جدا بخوان و هر دو را در سابقه بنویس.

مرحلهٔ دو: فرمان را روی کپی اجرا کن، نه روی فایل زنده

اجرای آزمایشی یعنی همان فرمان، روی همان الگو، روی یک نسخه، و بعد خواندن نتیجه. خروجی واقعی این مرحله، کد خروج نیست؛ خروجی، اختلاف نمایش‌داده‌شده است. آن را خط‌به‌خط بخوان، چون هر خطی که در انتظارش نبوده، یعنی الگو بیشتر از خواستهٔ تو دیده است.

cd ~
R=~/rollback/2026-11-30-title-escape
mkdir -p "$R/work" "$R/store"

F=public_html/wp-content/themes/child/inc/template-title.php
cp -p "$F" "$R/store/$(basename "$F").orig"
cp -p "$F" "$R/work/candidate.php"

sed -E 's/echo \$post->post_title;/echo esc_html( $post->post_title );/' \
  "$R/work/candidate.php" > "$R/work/candidate.sed.php"

diff -u "$R/work/candidate.php" "$R/work/candidate.sed.php"; echo "diff exit=$?"
php -l "$R/work/candidate.sed.php"
iconv -f UTF-8 -t UTF-8 "$R/work/candidate.sed.php" > /dev/null && echo "charset ok"
grep -n 'esc_html( \$post->post_title )' "$R/work/candidate.sed.php"

چهار چیزی که این ترتیب به تو می‌دهد، به همین ترتیب ارزش دارند:

اختلاف. حجم واقعی تغییر. اگر اختلاف خالی بود، الگو تطبیق پیدا نکرده و هیچ کاری نکرده‌ای؛ این همان حالت بی‌صدای خطرناک است که با موفق تمام می‌شود. اگر اختلاف بزرگ‌تر از انتظار بود، الگو دارد چیزهای دیگری را می‌خورد.

بررسی نحوی روی نسخهٔ تغییریافته. لازم است، کافی نیست. دلیلش را در بخش «بدترین حالت» باز کردم: ابزار نحوی، معنا را نمی‌بیند؛ و همین شکاف، موضوع یک مقالهٔ دیگر همین خوشه است که نشان می‌دهد چرا یک فایل آسیب‌دیده هم می‌تواند از بررسی نحوی سبز بیرون بیاید.

اعتبار رشتهٔ متن. iconv، تنها چیزی است که خراب شدن متن فارسیِ داخل فایل را نشان می‌دهد؛ بررسی نحوی این را نمی‌بیند.

محل نشانه‌گذاری. شمارش با موقعیت فرق دارد. بدترین حالت این است که الگو داخل یک توضیح کد یا داخل یک رشتهٔ ترجمه‌شدنی را عوض کرده باشد؛ آن‌جا نه خطا می‌دهی و نه کسی زود می‌فهمد.

روی کپی، اسکریپت اعمال را هم یک بار تمرین کن. یعنی اگر تغییر نهایی قرار است با یک اسکریپت اعمال شود، همان اسکریپت را روی پوشهٔ کپی اجرا کن و بعد کل درخت کپی را با درخت اصلی مقایسه کن. این مقایسه، تنها راهی است که می‌فهمی اسکریپت تو دقیقاً همان‌جاها را عوض می‌کند که گفته‌ای.

مرحلهٔ سه: اعمال، و نوشتنِ همان چیزی که عوض شد

اعمال وقتی مجاز است که سه شرط برآورده شده باشد: اختلاف کپی همان چیزی است که می‌خواهی، بررسی نحوی و رشته‌ای پاک است، و فهرست هدف با فهرست فایل‌هایی که اسکریپت دست می‌زند یکی است.

در لحظهٔ اعمال، دو کار هم‌زمان انجام بده. اول، فایل تغییریافتهٔ آزموده‌شده را جایگزین کن، نه اینکه دوباره روی فایل زنده الگو اجرا کنی. این تفاوت ظریف ولی تعیین‌کننده است: وقتی نسخهٔ آزموده‌شده را جایگزین می‌کنی، چیزی که روی دیسک می‌رود دقیقاً همان چیزی است که اختلافش را خط‌به‌خط خوانده‌ای. وقتی الگو را دوباره روی فایل زنده اجرا می‌کنی، یک بار دیگر شانس نادرست تعبیر شدن خریده‌ای، روی فایلی که دیگر نسخهٔ قبلی‌اش در دسترس نیست.

دوم، سابقه بنویس. برای هر فایل: مسیر، هش قبل، هش بعد، زمان، و نتیجهٔ نشانه. بدون هشِ بعد، در مرحلهٔ بعد نمی‌توانی ثابت کنی تغییر واقعاً روی دیسک نشسته است.

اعمال را با فرمانی انجام بده که خروجی‌اش را می‌شود در سابقه گذاشت، و اگر خطایی رخ داد، همان‌جا متوقف شود. ادامه دادن بعد از اولین فایل ناموفق، بدترین تصمیم در این مرحله است؛ چون نیمی از سایت را تغییر داده‌ای و نصف را نکرده‌ای، و این حالت از «همه غلط» هم بدتر است.

مرحلهٔ چهار: نشانه‌ای که فقط کد تازه می‌تواند تولید کند

در این مرحله فقط یک سوال جواب می‌خواهد: آیا کد تازه اجرا شد؟ برای آن باید نشانه‌ای بگذاری که تولید کردنش برای کد قدیمی غیرممکن باشد.

ساختار نشانه: یک رشتهٔ تصادفی یا زمان اجرا، که از داخل همان شاخهٔ کدی که تازه ساخته‌ای چاپ می‌شود. دو شکل قابل‌اتکا:

ثبت در لاگ سمت سرور. با فعال بودن ثبت خطا در فایل لاگ (WP_DEBUG_LOG)، یک خط از داخل همان شاخه‌ای که تازه نوشته‌ای ثبت کن؛ شکلی از این دست: error_log('depcheck ' . $TOKEN . ' from ' . __FILE__);. بعد همان رشته را در wp-content/debug.log بجوی. نوشتن __FILE__ در کنار نشانه، سوال مهم دیگری را هم جواب می‌دهد: کدام فایل واقعاً دارد اجرا می‌شود؛ چون در وردپرس، دیدنِ تغییر در فایل و بارگذاری‌شدنِ همان فایل، دو چیز متفاوت‌اند.

رشتهٔ قابل مشاهده در پاسخ. یک توضیح کوتاه و بی‌ضرر در خروجی، داخل همان قطعهٔ تغییریافته؛ شکلی مثل <!-- depcheck-${TOKEN} -->. بعد پاسخ را با ابزار خط فرمان بخوان. برای اینکه خودِ درخواست، کش صفحه را دور بزند، نشانه را در پارامتر درخواست هم بگذار تا پاسخ مجبور شود تازه تولید شود.

TOKEN=$(date -u +%s)
curl -s -D "$R/headers.txt" "${BASE_URL}/?depcheck=${TOKEN}" > "$R/body.html"
grep -c "depcheck-${TOKEN}" "$R/body.html"
grep "depcheck ${TOKEN}" public_html/wp-content/debug.log
grep -o 'x-litespeed-cache: [a-z]*' "$R/headers.txt"

در این چند سطر، BASE_URL را خودت مقدار بده؛ تنها چیزی که لازم است آدرس همان صفحه‌ای باشد که قطعهٔ تغییریافته را بارگذاری می‌کند. دو جستجوی بعدی، دو چیز متفاوت را ثابت می‌کنند و به‌همراهی‌شان مهم است: دیدنِ نشانه در بدنهٔ پاسخ یعنی صفحهٔ تازه تولید شده، و دیدنِ نشانه در لاگ یعنی کدِ تازه اجرا شده. یکی بدون دیگری کافی نیست؛ صفحهٔ تازه می‌تواند از قطعهٔ قدیمی ساخته شده باشد، و لاگ تازه می‌تواند از اجرایی بیرون از مسیر وب باشد. سطر آخر، هدرِ وضعیت کش را می‌خواند؛ همان مقدار، در مرحلهٔ تشخیص، مرز میان «کش سرو کرده» و «کش سرو نکرده» را مشخص می‌کند.

سه قانون دربارهٔ نشانه:

از کد قدیمی قابل تولید نباشد. اگر نشانه‌ات یک عدد ثابت است که در نسخهٔ قبلی هم بوده، چیزی ثابت نمی‌کند.

در همان بستهٔ تغییر حذف شود. نشانه‌ای که در محیط زنده جا بماند، هم یک اطلاعات فنی را علنی می‌کند و هم دیپلوی بعدی را گیج می‌کند. حذفش بخشی از کار است، نه کاری برای «بعداً».

نبودن نشانه یعنی کار تمام نشده. این جمله را جدی بگیر: نبودنِ نشانه را با موفقیت اشتباه نگیر، و بالا بودنِ سایت را دلیل بر بی‌نیازی از نشانه ننویس.

جدول مراحل؛ فرمان، دلیل باورکردنی، شرط توقف

| مرحله | فرمان | چه چیزی ثابت می‌کند که کار کرده | شرط توقف | |—|—|—|—| | 1 ثبت وضعیت | sha256sum و stat -c روی فهرست هدف | مبنای مقایسه داری، و مجوزها ثبت شده‌اند | شمار تطبیق با انتظار نمی‌خواند | | 2 اجرای آزمایشی | sed روی کپی، بعد diff -u | حجم تغییر همان چیزی است که می‌خواستی | اختلاف خالی، یا بزرگ‌تر از انتظار | | 2 بررسی فایل | php -l و iconv | نحوی سالم و متن فارسی سالم | هر دو باید پاک باشند، نه یکی | | 3 اعمال | جابه‌جایی نسخهٔ آزموده‌شده و نوشتن سابقه | هش بعد در سابقه با فایل زنده یکی است | خطا در فایل اول؛ ادامه نده | | 4 تأیید اجرا | جستجوی نشانه در لاگ و در پاسخ | کد تازه اجرا شده، نه فقط موجود روی دیسک | نشانه نیست؛ برو بخش تشخیص | | 5 مقایسهٔ درخت | sha256sum کل پوشهٔ قالب، قبل و بعد | هیچ فایل دیگری دست نخورده است | هر فایل خارج از فهرست هدف تغییر کرده | | 6 بازگشت | کپی فایل هدف از بیرون وب | هش فایل زنده با هش قبل یکی شد | هش بعد از بازگشت نمی‌خواند |

این جدول را در همان پوشهٔ سابقه کپی کن. روزی که تحت فشار هستی، ترتیب را از همین جدول بخوان، نه از حافظه.

اگر نشانه پیدا نشد؛ ترتیب تشخیص

اینجا جایی است که بیشتر وقت‌ها می‌سوزد، چون واکنش اولِ همه «دوباره دیپلوی کن» است و این بدترین کار ممکن است. دیپلوی دوم روی تشخیص ناقص، هم وضع را بدتر می‌کند و هم نشانه‌های باقی‌مانده را پاک می‌کند. به‌جایش، این پنج سوال را به همین ترتیب جواب بده.

یک: آیا اصلاً تطبیقی انجام شد؟ روی فایل زنده بشمار. اگر صفر است، الگو هیچ‌جا را ندیده؛ یعنی تغییر اعمال نشده و کش بی‌گناه است. دلیلش معمولاً فرق جزئی در فاصله، در علامت سطر، یا در متنی است که تو از یک نسخهٔ دیگر کپی کرده‌ای.

دو: آیا فایل درستی را عوض کرده‌ام؟ مسیر واقعی بارگذاری را از لاگ نشانه بخوان، نه از آنچه فکر می‌کنی. در وردپرس این حالت زیاد است: فایل در پوشهٔ قالب اصلی عوض شده، اما همان قطعه از قالب فرزند یا از یک نسخهٔ رهاشدهٔ دیگری بارگذاری می‌شود. اگر نشانه در لاگ نیست ولی در فایل هست، این سوال اولویت توست.

سه: کدام لایهٔ کش دارد پاسخ قدیمی می‌دهد؟ دو لایه را جدا کن. لایهٔ کد اجراییِ ذخیره‌شده در حافظه، و لایهٔ صفحه‌های آماده. نشانه‌شان یکی نیست: اگر هدر وضعیت کش بگوید پاسخ تازه تولید شده و باز رفتار قدیمی است، مشکلت لایهٔ کد ذخیره‌شده در حافظه است؛ اگر پاسخ از کش سرو شده، لایهٔ صفحه. برای لایهٔ اول، مسیر بی‌خطر، ری‌استارت workerهای همان حساب است، نه سرویس سراسری. این دو لایه و روش خواندنشان، موضوع یک مقالهٔ دیگر همین خوشه است.

چهار: آیا فایل روی دیسک معتبر است؟ بررسی نحوی و اعتبار رشته‌ای را روی فایل زنده هم اجرا کن، نه فقط روی کپی. اگر کپی سالم بود و زنده نه، یعنی فایل دیگری در همان الگو افتاده است.

پنج: آیا تغییر به دیتابیس هم نیاز داشت؟ اگر بخشی از کار در جدول گزینه‌ها بوده و تو فقط فایل را عوض کرده‌ای، رفتار سایت همان تغییر نیافتهٔ دیتابیس است، نه کد.

بعد از این پنج، هر کدام از لایه‌ها که متهم شد را جدا بسنج: یک لایه را بی‌اثر کن و دوباره نشانه را بخوان. تغییر یک‌بارهٔ چند لایه، تشخیص را غیرممکن می‌کند.

بازگشت دقیق؛ همان فایل را برگردان، نه کل سایت

بازگشت، نباید بزرگ‌تر از تغییر باشد. اگر یک فایل را عوض کرده‌ای، بازگشت یعنی همان یک فایل برگردد. برای رسیدن به این وضعیت، همان سابقهٔ مرحلهٔ یک لازم است: مسیر کپی، هش قبل، و مجوز قبل.

cd ~
R=~/rollback/2026-11-30-title-escape
TARGETS="$R/targets.txt"

: > "$R/after.sha256"; : > "$R/after.perm"
while IFS= read -r f; do
  sha256sum "$f" >> "$R/after.sha256"
  stat -c '%a %U:%G %n' "$f" >> "$R/after.perm"
done < "$TARGETS"

paste "$R/before.sha256" "$R/after.sha256" | awk '$1 != $3 { print "changed: " $2 }'
diff "$R/before.perm" "$R/after.perm"

find public_html/wp-content/themes/child -type f -name '*.php' -exec sha256sum {} \; > "$R/tree.after"
diff "$R/tree.before" "$R/tree.after"

این مقایسه، پاسخ یک سوال است که در روز خرابی مطرح می‌شود: دقیقاً چند فایل عوض شده؟ بخش میانی، فقط فایل‌های فهرست هدف را می‌سنجد؛ دو سطر آخر، کل درخت قالب را. اگر فایلی بیرون از فهرست تو تغییر کرده باشد، همان‌جا لو می‌رود. اگر فهرست تو دو فایل داشته و مقایسهٔ درخت، سه فایل تغییریافته نشان دهد، بازگشت تو باید هر سه را پوشش دهد، نه دوتایشان؛ و این حالت دقیقاً از اجرای الگو روی فایل‌های خارج از انتظار می‌آید. این مقایسه را بعد از هر اعمال اجرا کن، نه فقط بعد از شک.

وقتی خواستی برگردی، ترتیب ثابت است: فایل هدف را از کپیِ بیرونِ وب جایگزین کن، مجوز را از سابقه بازگردان، هش بگیر و با هشِ «قبل» مقایسه کن، لایه‌های کش را بی‌اثر کن، و در گام آخر بررسی کن که نشانهٔ کد تازه دیگر تولید نمی‌شود. این گام آخر را جدی بگیر؛ اگر بعد از بازگشت نشانه هنوز در لاگ ظاهر می‌شود، یعنی بازگشت کامل نبوده یا فایل دیگری در همان مسیر اجرا می‌کند.

چرا بازگردانی کل بکاپ، جواب این حالت نیست؟ سه دلیل عملی. اول، یک نسخهٔ کلی، سایت را به لحظهٔ گرفتنش می‌برد؛ یعنی محتوایی که بعد از آن منتشر شده و هر داده‌ای که در فاصله نوشته شده، می‌رود. دوم، روی هاست اشتراکی، بازگردانی کل حساب ساعت‌ها طول می‌کشد و در آن مدت سایت یا پایین است یا نیمه‌تمام. سوم، و مهم‌تر: اگر تغییرت فقط یک فایل بوده، بازگردانی کلی، ریسک را از یک فایل به تمام سایت منتقل می‌کند. بازگردانی نسخهٔ کلی، ابزار حالتی است که نمی‌دانی چه چیزی خراب شده؛ نه ابزاری برای وقتی که می‌دانی.

استثنای صادقانه: اگر تغییر شامل دیتابیس بوده، یا اگر دامنهٔ تغییر را نمی‌دانی چون فهرست هدف ننوشته‌ای، بازگشت دقیق ممکن نیست. آن‌جا همان مسیر نسخهٔ آزموده‌شده را برو، و از همان روز اول فهرست هدف را جدی بگیر.

نسخهٔ پشتیبان را بیرون از درختی بگذار که جستجو می‌کنی

در بخش قبلی گفتم کپی باید بیرون از وب بماند. این همان نکته‌ای است که در این روش بیش از همه نادیده گرفته می‌شود، چون فرمان‌های بازنویسی معمولاً خودشان کنار فایل اصلی کپی می‌سازند و آن کپی، داخل درخت وب است. چهار خسارت، همه واقعی:

قابل سرو شدن. فایلی که کنار فایل اصلی با پسوند اضافه مانده، معمولاً دیگر توسط مفسر اجرا نمی‌شود؛ به‌صورت متن خام سرو می‌شود. یعنی منبع درونی قالب یا افزونه‌ات، برای هر کسی که آدرس را پیدا کند، خوانده می‌شود. بدترین حالتِ همین الگو، فایلی است که تنظیمات اتصال و کلیدهای حساس را در خودش دارد؛ همان یک فایل، کل سایت را از یک نسخهٔ پشتیبانِ فراموش‌شده، قابل خواندن می‌کند.

قابل خزیدن. هر کپی داخل درخت وب، یک روز در ایندکس جستجو می‌افتد: یا از یک پیوند باقی‌مانده، یا از لاگ، یا شانسی. نتیجه، محتوای تکراری بیرون از کنترل توست، بدون اینکه بدانی کدام آدرس‌ها پدید آمده‌اند.

قابل جستجو. هر ابزار و افزونه‌ای که روی درخت فایل یا روی محتوا ایندکس می‌سازد، این کپی‌ها را هم می‌بیند. در جستجوی درونی سایت، نسخهٔ قدیمیِ یک نوشته یا یک فایل، نتیجه می‌شود؛ و کاربر چیزی را باز می‌کند که قرار بود حذف شده باشد.

قابل بازنویسی. این آن خسارتی است که مستقیماً به همین مقاله برمی‌گردد. روز بعد، یک بازنویسی دست‌جمعی روی همان پوشه اجرا می‌شود و چون الگوی جستجو فایل‌های کپی را هم می‌بیند، آن‌ها را عوض می‌کند. نتیجه: نسخهٔ سالمی که برای بازگشت نگه داشته بودی، دیگر سالم نیست، و تو در لحظهٔ نیاز، یک کپی تغییریافته داری که باورش نمی‌کنی.

قانون ساده: هر چیزی که برای بازگشت است، بیرون از درخت وب و بیرون از دامنهٔ جستجوی ابزارها باشد. زیر پوشهٔ حساب، در مسیری زمان‌دار، با ساختاری که از نامش پیداست هر کپی متعلق به کدام فایل زنده بوده. وقتی مجبوری برای یک لحظه کپی داخل درخت نگه داری، نامی بده که نه سرو شود نه با الگوی جستجوی تو بخورد، و حذفش را به همان مرحلهٔ پاک‌سازیِ جداگانه بسپار.

دستور پاک‌سازی را داخل اسکریپت اعمال نگذار

این اشتباه، شکل ظاهری تمیز دارد: اسکریپت اعمال در آخرین خط، فایل‌های اضافی را پاک می‌کند تا پوشه مرتب بماند. و دقیقاً همین خط، روز بد تو را نابود می‌کند.

سناریو محتمل است: فردا کسی همان اسکریپت را دوباره اجرا می‌کند، فقط برای اینکه مجوزها را درست کند. یا همکار تو، برای اعمال یک تغییر جزئی دیگر، همان فایل را از نو صدا می‌زند. یا بخشی از ابزار نصب، در یک اجرای مجدد، پاک‌سازی را هم با خودش می‌آورد. در هر سه حالت، مسیر بازگشت پاک شده؛ و تو بدون نسخهٔ قبلی، با فایلی که نمی‌دانی قبلاً چه بوده، می‌مانی.

سه قانون:

اسکریپت اعمال فقط رو به جلو بنویسد و ثبت کند. هیچ دستور حذف در آن نباشد. اگر لازم داری چیزی کم شود، در مرحلهٔ بعدی و با فرمانی جدا انجام شود.

پاک‌سازی، فرمان مستقل و خوانا دارد. نامش طوری باشد که در فهرست فایل‌ها، شبیه «مرتب کردن» به نظر نرسد. قبل از حذف، فهرست آنچه می‌خواهد حذف کند را چاپ کند و تا تأیید صریح تو اجرا نشود.

معیار اجرای پاک‌سازی، تأیید باشد نه زمان. پاک‌سازی باید چیزی را بررسی کند که نشانهٔ تأیید در آن ثبت شده باشد؛ اگر آن سابقه نیست، فرمان باید خودداری کند. این تنها راهی است که پاک‌سازی به یک عادت خطرناک تبدیل نمی‌شود.

نگه‌داشتن نسخهٔ قبلی تا یک روز کاری کافی نیست. شرط معقول‌تر این است: تا وقتی یک چرخهٔ کامل ترافیک از حالت تازه گذشته و یک نسخهٔ پشتیبان تازه، آن حالت را در خودش داشته باشد. چون بخشی از خرابی‌های این روش، در اولین بازدید دیده نمی‌شوند؛ در اولین استفادهٔ واقعی یک کاربر، یا در اولین گزارش میزبان.

دیپلوی باید دوباره‌اجراشدنی باشد

اجرای دوم نباید چیزی خراب کند. این ویژگی، نه یک انتخاب تمیزی کار، بلکه بخشی از طراحی است: روی هاست اشتراکی، اسکریپت تو ممکن است دو بار اجرا شود؛ از روی عجله، از روی شک، یا توسط کسی که نمی‌داند قبلاً اجرا شده.

بدترین حالت واقعی، الگویی است که متن جایگزین، همان عبارت جستجو را در خودش دارد. اجرای اول درست است؛ اجرای دوم، نتیجه را تودرتو می‌کند. یعنی یک فراوری دو بار روی همان داده اعمال می‌شود و خروجی، رشته‌ای می‌شود که دو بار کدگذاری شده است. سایت بالا است، هیچ خطایی نیست، و کاربر متن زشت می‌بیند.

cd ~/rollback/2026-11-30-title-escape/work
sed -E 's/\$post->post_title/esc_html( $post->post_title )/g' candidate.php > run1.php
sed -E 's/\$post->post_title/esc_html( $post->post_title )/g' run1.php     > run2.php
grep -c 'esc_html' run1.php run2.php
php -l run2.php

خروجی این چهار سطر، همهٔ حرف بخش است: run2.php از نظر نحوی سالم گزارش می‌شود، درحالی‌که مقدارش فاجعه است. این همان حالتی است که بازنویسی درجا را ناامیدکننده می‌کند: خرابی در لایهٔ معناست، نه در لایهٔ ساختار، و ابزار ساختار، آن را نمی‌بیند.

راه‌های ساختن اسکریپت دوباره‌اجراشدنی، به ترتیب ترجیح:

اول: کل فایل را جایگزین کن، نه بخشی از آن را. نسخهٔ آزموده‌شده را از پوشهٔ کاری خودت سرِ فایل زنده بگذار. این روش ذاتاً دوباره‌اجراشدنی است؛ اجرای دوم، همان فایل را دوباره می‌گذارد و نتیجه عوض نمی‌شود.

دوم: با مقایسهٔ هش تصمیم بگیر. اگر هش فایل زنده با هش «بعد» در سابقه یکی است، آن فایل اعمال شده؛ رد شو و در خروجی بنویس که رد شده. اگر با هش «قبل» یکی است، اعمال کن. این دو سطر، کل منطق دوباره‌اجراشدنی را می‌سازند.

سوم: اگر اصرار به ویرایش درجا داری، الگو را طوری لنگر کن که جایگزین، دوباره نخورد. یعنی بخشی از زمینهٔ اطراف را هم در الگو ببین، به‌طوری‌که متن تولیدشده دیگر با همان الگو تطبیق نیابد.

و در هر سه حالت، سابقه بنویس: چه چیزی، چه زمانی، و از چه هشِ پیشین به چه هشِ پسین. اسکریپتی که هیچ اثری از اجرای خودش روی دیسک نمی‌گذارد، در روز خرابی، قابل دفاع نیست. تست نهایی هم یک خط دارد: اسکریپت را دو بار اجرا کن، و بعد کل درخت را با مقایسهٔ هش بسنج؛ اجرای دوم باید صفر تغییر نشان دهد.

چه چیزی را نباید بپذیری

چهار جملهٔ رایج که در این مسیر زیاد می‌شنوم و همه‌شان یک نقص مشترک دارند: هزینهٔ شکست را پنهان می‌کنند.

«ویرایش درجا سریع‌ترین راه است، پس بهتر است.» سریع‌ترین بله. ولی سرعتی که ثبتِ بازگشت ندارد، فقط خسارت را دیرتر و گران‌تر می‌کند. اگر قرار است همین کار را بکنی، سریع‌ترین نسخهٔ امن همان است: روی کپی امتحان کن، بعد جایگزین کن؛ فقط دو دقیقه اضافه دارد.

«هر چیزی خراب شد، از بکاپ برمی‌گردانیم.» این جمله سه فرض دارد که باید ثابت شوند: نسخهٔ قابل‌اتکا داری، بازگردانی‌اش را آزموده‌ای، و حاضری داده‌های تولیدشده در فاصله را از دست بدهی. تا این سه جواب نداشته باشند، بازگشت دقیق، تنها گزینهٔ واقعی است.

«فایل را نگاه کردم، درست است.» نگاه کردن به فایل، اجرای آن را ثابت نمی‌کند. کد ذخیره‌شده در حافظه و صفحهٔ آماده، هر دو می‌توانند درست‌ترین فایل تاریخ را بی‌اثر نشان دهند. نشانه، تنها جواب است.

«سایت که تغییری نکرد، پس دیپلوی شکست خورده؛ دوباره اجرا کن.» در تجربهٔ من، «بی‌اثر» بیشتر لایهٔ کش است تا دیپلوی ناقص. واکنش درست، تشخیص لایه است، نه تکرار تغییر. اجرای دوباره روی تشخیص ناقص، همان چیزی است که خرابی را از یک فایل به کل درخت گسترش می‌دهد.

ترتیبی که باید در اسناد پروژه بماند

این بخش را برای همان روزی نوشته‌ام که تحت فشاری. کل روش در هفت خط:

فایل‌هایی که قرار است عوض شوند را در یک فهرست بنویس، بیرون از درخت وب. هش و مجوز همان فایل‌ها را ثبت کن. شمار تطبیق را با انتظارت مقایسه کن؛ نخورد، توقف کن. فرمان را روی کپی اجرا کن و اختلاف را خط‌به‌خط بخوان؛ نحوی و اعتبار رشته‌ای را روی همان کپی بسنج. نسخهٔ آزموده‌شده را جایگزین کن و هش بعد را در سابقه بنویس. نشانه‌ای بگذار که فقط کد تازه تولیدش می‌کند و وجودش را هم در لاگ و هم در پاسخ تأیید کن؛ بعد نشانه را در همان بستهٔ کار بردار. پاک‌سازی را هرگز در مسیر اعمال نگذار.

اگر این هفت خط را داشته باشی، تغییر تو از یک شرط‌بندی به یک عملیات تبدیل می‌شود. اگر نداشته باشی، تنها چیزی که در روز خرابی داری، یک بکاپ کلی است و یک روز از دست‌رفته.

دو مورد از همین خوشه را قبل از هر کاری بخوان: راهنمای بکاپ و تست بازگردانی روی سی‌پنل، که ثابت می‌کند نسخه‌ای که به آن تکیه می‌کنی باز می‌گردد یا نه؛ و کش کد اجرایی و دلیل بی‌اثر به‌نظررسیدن دیپلوی، که همان لایه‌ای است که در مرحلهٔ چهار متهم اول می‌شود. اگر می‌خواهی اصلاً این خطر را از مسیر زندگی‌ات بیرون ببری، محیط تست روی سی‌پنل را بساز و تغییر را آن‌جا امتحان کن؛ اجرای آزمایشی روی کپی، جایگزین آن محیط است، نه برابرش.

مقالبی بخوانی

من متخصص سئو که عاشق اینه به کسب‌وکارها کمک کنه تو گوگل دیده بشن!از وردپرس گرفته تا سئو، هرچیزی که برای رشد آنلاین نیاز داری میتونم کمکت کنم.

دیدگاه خود را بنویسید