Diagnosis + fix data Rails produksi: kamal exec, `?` fullwidth, Base64, kontinuitas thread.
Produksi 400/500 sama sekali gak mirip lokal. Di lokal lo jalanin test ulang, edit, jalanin lagi. Di produksi, user udah klik "Bayar" dan lagi nontonin layar kosong.
Tiga jalur:
rails console, perubahan lo gak ke-audit, salah ketik sekali lo matiArtikel ini soal jalur ketiga. Dua kasus nyata: ? fullwidth yang nyantol di credentials sampe pembayaran x402 jadi 500, dan tweet di tengah thread yang status: :failed harus dikirim ulang sambil jaga kontinuitas thread. Di sepanjang jalan, Claude bakal coba salah arah di 4 arah berbeda by default — lo harus intercept satu-satu.
kamal app exec --reuseKamal adalah tool deploy Rails dari 37signals. Dia punya command:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse artinya: jangan bangunin container baru, eksekusi di dalam container web yang lagi jalan. Tanpa build baru, tanpa docker pull, tanpa restart, tanpa re-injeksi ENV. Command jalan, container lanjut nangani request.
Outputnya nyampe stdout lo lewat terminal — efektif puts di dalam Rails console produksi, tanpa SSH, tmux, atau ninggalin laptop.
Sesi tipikal:
$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847
Bolak-balik 3-6 detik. Dua orde magnitudo lebih cepet dari redeploy.
? Fullwidth yang Bikin 500 di ProduksiCommit eba9ac9.
Gejala: Beberapa jam setelah rilis pembayaran x402, tiap request pembayaran jadi 500. Log penuh Net::HTTPBadResponse dari HTTP client. Lokal jalan sempurna.
Diagnosis: Suruh Claude print konfigurasi x402 di produksi dulu:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Output:
"0xAbC123...def?"
Ada ? ekstra di ujung — tanda tanya fullwidth (U+FF1F), bukan ? halfwidth. IME seseorang switch pas lagi edit config/credentials/production.yml.enc, karakternya keselip masuk.
config/credentials.yml.enc lokal (yang didekrip pake master.key dev/test) gak punya ini — production sama dev di Rails 8 adalah encrypted credentials terpisah, kontennya gak sharing.
Fix: Lo gak bisa SSH dan edit file langsung (udah dienkripsi) dan lo gak bisa pull ke lokal dan edit (master.key gak ada di laptop). Jalannya adalah Claude nulis script Ruby sekali pakai dan inject via EDITOR= ke credentials:edit:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Hilangin tanda tanya fullwidth di ujung
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
bin/rails credentials:edit --environment production
Alur credentials:edit: dekrip → tulis ke file temp → panggil $EDITOR → re-enkrip → hapus temp. Tuker EDITOR pake script Ruby kita dan edit teratomatisasi, gak perlu melototin ciphertext di lokal.
Setelah itu, git commit + kamal deploy sekali — deploy ini wajib karena production.yml.enc berubah. Tapi diagnosis gak makan deploy sama sekali.
Aturan: Pas produksi rusak, baca dulu pake kamal app exec --reuse. Jangan nebak, jangan redeploy duluan.
Gejala: Setelah artikel publish, tweet masuk ke tabel x_queue_tweets. Salah satunya — yang ke-2 dari thread 4 — berakhir status: :failed (rate limit X API, validasi konten, apapun). Retry-nya butuh jaga kontinuitas thread sama tweet ke-1.
Cari yang gagal:
kamal app exec --reuse 'bin/rails runner "
XQueue::Tweet.where(status: :failed).each do |t|
puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
end
"'
Ternyata id=87, thread_id=15, thread_position=2.
Jebakan 1: escape shell. Konten tweet sering ada kutipan, newline, backtick. Kalo lo nulis:
# meledak — shell makan kutipan dan backslash
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
Tarian Base64 — encode di lokal, lempar string base64, decode di dalam runner:
# Encode lokal
echo -n 'Konten tweet yang ditulis ulang...' | base64
# => S29udGVuIHR3ZWV0IHlhbmcgZGl0dWxpcyB1bGFuZy4uLg==
# Kirim
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('S29udGVuIHR3ZWV0IHlhbmcgZGl0dWxpcyB1bGFuZy4uLg=='), status: :scheduled)
puts t.status
\""
String Base64 itu ASCII-only, aman buat shell.
Jebakan 2: kontinuitas thread. XQueue::PostTweetJob.perform_later(87) ngepost tweet sendirian — gak nyambung ke tweet #1 — karena X API butuh reply_to_tweet_id, dan Job default-nya gak bawa ini.
Cari x_tweet_id tweet sebelumnya (yang udah terkirim sukses ngisi field ini):
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789
Enqueue sambil bawa target reply:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
polling_interval worker adalah 0.1 detik — nge-pickup job hampir instan. Beberapa detik kemudian, kamal app exec buat cek status flip dari scheduled ke posted dan x_tweet_id keisi — thread tersambung.
Aturan: Operasi data produksi harus ngormatin constraint layer bisnis, bukan cuma "record berhasil diupdate". Kontinuitas thread itu constraint bisnis; Rails runner gak ngecek itu buat lo.
Pas ngerjain kerjaan kayak gini, insting pertama Claude sering salah. Tangkep satu-satu:
"Gue SSH dulu ke server buat liat..."
Arahin ulang: kamal app exec --reuse ngalahin SSH — di dalam container, env Rails udah ter-load, ter-audit (log kamal nyimpen record), gak nyentuh shell host, gak ada container drift (reuse ngejamin versi produksi saat ini).
"Gue tulis migration buat ngapus
?fullwidth dari wallet_address..."
Arahin ulang: Nukar satu nilai credentials gak butuh migration (DB gak disentuh). Rails runner sekali pakai selesai dalam 10 detik; migration butuh deploy dan hidup di schema selamanya. Pake migration cuma kalo fix-nya bisa aja mesti diulang; typo itu urusan sekali jadi.
"Tinggal
update!(content: "...")aja..."
Arahin ulang: Konten apapun yang dibuat user (tweet, komen, markdown input user) harus lewat Base64. Parse shell buat kutipan, backslash, $, dan backtick itu ranjau klasik — produksi bukan tempat buat latihan.
perform_later tanpa parameter bisnis"Tinggal
PostTweetJob.perform_later(87)lagi..."
Arahin ulang: Tanya dulu "record ini nyambung ke yang lain gak?" Thread punya relasi reply_to, batch job punya batch_id, paginated job punya cursor — daftar argumen Job itu pembawa relasi-relasi itu. Skip satu argumen, putus satu rantai.
Debug + mutasi data Rails produksi pake Claude — 6 aturan:
kamal app exec --reuse 'bin/rails runner "puts X"'. Lokalisir masalah sebelum ubah apapun.kamal app exec --reuse itu alat default, bukan SSH, bukan redeploy. Di container, Rails ter-load, bolak-balik 3-6 detik.EDITOR=ruby-script bin/rails credentials:edit --environment production. Script Ruby ngerjain edit-nya, tanpa melototin ciphertext lokal.echo -n 'X' | base64 di lokal, Base64.decode64 di runner.Debug di produksi bukan mindahin alur dev ke produksi — lo gak punya ruang iterasi, lo gak punya toleransi error. Yang beneran lo pake itu surface introspeksi yang udah disediain produksi (Rails runner + kamal exec + credentials:edit), tiap langkah bikin perubahan sekecil mungkin. Claude bisa nulis Ruby yang bener — tapi ngerti "apa yang bisa langsung dilakuin vs harus didiagnosis dulu" itu keputusan yang dia gak buatin buat lo. Itu disiplin produksi lo.