У ошибок деплоя нет симптомов. Четыре ограждения — разделённые vault'ы, EDITOR-скрипты, проверка обратным чтением, рантайм-флип по env — чтобы использовать Claude, не спалив всё.
Главное отличие деплоя от написания кода: деплой — один выстрел, высокие ставки, откатывать больно. Напишешь сломанную строчку кода — один прогон тестов её ловит. Напишешь сломанную строчку credentials — узнаешь только при первой реальной оплате: пользователи списали с карты, деньги не дошли до твоего счёта, логи — сплошные 400.
Недавно перетащил how2claude с локального dev'а в продакшен вместе с Claude: Stripe live-аккаунт, x402 mainnet кошелёк, Google OAuth, Kamal secrets. Claude не знает, какие значения тестовые, а какие настоящие. Не знает, что sk_live_ vs sk_test_ — разница в одну букву с апокалиптическими последствиями. Ограждения ставишь ты.
Rails по умолчанию использует config/credentials.yml.enc, расшифровывается через config/master.key. Если используешь этот дефолт — ты проиграл.
Если ты положишь sk_live_xxx в этот файл и прогонишь тесты локально, твой тестовый код дергает продакшен Stripe. Каждый прогон списывает с реальной карты.
Разделяй на два:
- config/credentials.yml.enc + config/master.key: dev/test, хранит sk_test_xxx
- config/credentials/production.yml.enc + config/credentials/production.key: prod, хранит sk_live_xxx
Оба .key в .gitignore, оба .enc закоммичены. production.key живёт только на деплой-машине.
Дальше Kamal secrets должен указывать на правильный файл:
# .kamal/secrets
-RAILS_MASTER_KEY=$(cat config/master.key)
+RAILS_MASTER_KEY=$(cat config/credentials/production.key)
Внутри контейнера RAILS_MASTER_KEY теперь указывает на продакшен-ключ, и расшифровываются продакшен-credentials. Я смотрел, как Claude пишет эту строку — дефолтный шаблон Kamal — config/master.key (dev'овский), который молча деплоит dev Stripe key в продакшен.
bin/rails credentials:edit --environment production открывает интерактивный редактор. Claude интерактивный редактор не водит. Ты тоже не хочешь вручную вставлять десяток секретов (одна опечатка всё ломает).
Используй этот паттерн:
EDITOR="ruby script/set_prod_webhook_secret.rb" \
bin/rails credentials:edit --environment production
rm script/set_prod_webhook_secret.rb
script/set_prod_webhook_secret.rb выглядит так:
# ARGV[0] — путь к временному расшифрованному YAML-файлу
file = ARGV[0]
require "yaml"
data = YAML.load_file(file) || {}
data["stripe"] ||= {}
data["stripe"]["webhook_secret"] = "whsec_GHWObNAKFh2HPOlJpbGmlYfIiKz1C8EY"
File.write(file, data.to_yaml)
Rails пишет расшифрованный YAML во временный файл, вызывает твой «EDITOR» с этим путём как аргумент, на выходе шифрует обратно. Твой «EDITOR» — просто Ruby-скрипт, хирургически меняющий один ключ и сохраняющий.
Почему хорошо:
- Точно: трогает только stripe.webhook_secret, ничего больше.
- Идемпотентно: прогнать дважды — как прогнать один раз.
- Аудируемо: скрипт и есть diff. Claude пишет, ты взглянул — и точно знаешь, что он поменяет.
- Исчезает при удалении: после rm нет plaintext-credential'а на диске, нет пасты whsec_... в истории шелла.
По скрипту на credential: set_stripe_live_key.rb, set_webhook_secret.rb, set_price_ids.rb, set_wallet_address.rb. Каждый сразу rm.
Написал — не верь, что написал правильно. Прочитай обратно:
bin/rails runner -e production "
c = Rails.application.credentials
puts 'sk_live set: ' + c.dig(:stripe, :secret_key).to_s.start_with?('sk_live_').to_s
puts 'webhook_secret set: ' + c.dig(:stripe, :webhook_secret).to_s.start_with?('whsec_').to_s
puts 'wallet prefix: ' + c.dig(:x402, :wallet_address).to_s[0..5]
puts 'wallet len: ' + c.dig(:x402, :wallet_address).to_s.length.to_s
"
Ключевое — проверки префикса и длины, а не просто распечатать значения.
sk_live_. Если обратно читается sk_test_, Claude положил test key в prod-файл — этот баг ты не найдёшь до первого реального платежа.whsec_. Правильный формат или нет — видно с одного взгляда.0x.... Неправильная длина — значит, пробрался посторонний символ.Добавление проверок префикса блокирует большинство опечаток, пропущенных полей и перепутанных окружений.
Соблазн подумать: «перед деплоем переключу network на mainnet». Такой переключатель держится на человеческой памяти. Рано или поздно обожжёшься.
Впечатай правило в initializer:
# config/initializers/x402.rb
X402.configure do |c|
c.wallet_address = Rails.application.credentials.dig(:x402, :wallet_address)
c.chain = Rails.env.production? ? "base" : "base-sepolia"
end
Тот же код, testnet (sepolia) в dev, mainnet (base) в prod. При деплое ничего менять не надо. Claude не может «забыть переключить», потому что переключать — не его работа.
Тот же приём для basescan_tx_url, видимости Plan (dev-only планы не рендерятся в prod), выбора Stripe price ID и так далее. Всё, что отличается между dev и prod, флипай через Rails.env. Не полагайся на то, что вспомнишь при деплое.
Даже с четырьмя ограждениями первый клик платежа настоящими деньгами — за тобой.
В предыдущей статье Отлаживаем тихие баги с Claude я рассказал про первый клик x402-платежа в проде: invalid_string at payTo в консоли. 43-й символ адреса кошелька — знак вопроса полной ширины, залетевший из китайского IME. Проверки префикса его не ловили (0x на месте), тесты не ловили (тесты не стреляют реальными транзакциями), только реальный клик вытащил его наружу.
Ограждения деплоя — не для Claude. Они для тебя — автоматизируй то, что можно автоматизировать (префиксы, длины, env-флипы), чтобы сэкономленное внимание шло на реальные взаимодействия, которые автоматизация не покрывает.
Полный флоу деплоя с Claude:
.enc + .key файлы.EDITOR=script, сразу удаляй.Rails.env, не при деплое.Деплой — это не «писать код с более высокими ставками». Деплой — это «писать код, где никто не говорит тебе, что ты неправ». Ограждения нужны, чтобы превратить «никто не говорит» в «ты узнаёшь за 15 секунд».