Eric Guo's blog.cloud-mes.com

Hoping writing JS, Ruby & Rails and Go article, but fallback to DevOps note

Migrate From Yarn to Pnpm Package Manager in the Rails Way

Permalink

After evaluating package managers for our Rails project, I decide migrated from Yarn to pnpm for stricter, faster, and more disk‑efficient installs.

This guide is organized into four parts so you can adopt pnpm in the right order.

Why pnpm

  • Disk efficiency: A content‑addressable store and hardlinks dramatically reduce duplicate files.
  • Speed: Faster cold and warm installs with solid caching.
  • Strictness: Fails on missing/phantom deps and ensures reproducible installs.
  • Workspaces: First‑class monorepo support without heavy hoisting.

And the key reason: yarn is not doing any release in past two years and META just lay-off huge people recently.

How to do import in Node.js side

  1. Install pnpm (prefer Corepack):
# If Node comes with Corepack (Node 16.13+)
corepack enable
corepack prepare pnpm@latest --activate
pnpm --version # should avtivate now
# Otherwise fall back to npm with global
npm i -g pnpm@latest
  1. Import your existing Yarn lockfile first:
# Generates pnpm-lock.yaml from yarn.lock
pnpm import
git add pnpm-lock.yaml
  1. Remove Yarn artifacts and switch to pnpm in package.json:
git rm yarn.lock
{
"packageManager": "pnpm@10"
}
  1. Clean and install deterministically:
rm -rf node_modules .yarn .yarn-cache
pnpm install --frozen-lockfile
pnpm approve-builds
  1. (Optional) Workspaces: add pnpm-workspace.yaml in a monorepo:
# Only enable if you meeting build problem as pnpm by default using isolated
# https://pnpm.io/settings#nodelinker
# nodeLinker: hoisted
packages:
- '.'
# - 'packages/**'

How to change Rails to using pnpm

  1. Remove Yarn‑specific deploy helpers (if present):
Gemfile
- gem 'capistrano-yarn'
+ gem 'capistrano-pnpm'
Capfile
- require 'capistrano/yarn'
+ require 'capistrano/pnpm'
  1. Add a bin/pnpm wrapper so Rails tasks can call pnpm consistently:
#!/usr/bin/env ruby
APP_ROOT = File.expand_path('..', __dir__)
Dir.chdir(APP_ROOT) do
pnpm = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR)
.reject { |dir| File.expand_path(dir) == __dir__ }
.product(%w[pnpm pnpm.cmd pnpm.ps1])
.map { |dir, file| File.expand_path(file, dir) }
.find { |file| File.executable?(file) }
if pnpm
exec pnpm, *ARGV
else
$stderr.puts 'pnpm executable was not detected in the system.'
$stderr.puts 'Install pnpm via https://pnpm.io/installation'
exit 1
end
end

Make it executable: chmod +x bin/pnpm.

  1. Keep backward compatibility by proxying bin/yarn to pnpm (update existing file):
#!/usr/bin/env ruby
APP_ROOT = File.expand_path('..', __dir__)
Dir.chdir(APP_ROOT) do
pnpm = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR)
.reject { |dir| File.expand_path(dir) == __dir__ }
.product(%w[pnpm pnpm.cmd pnpm.ps1])
.map { |dir, file| File.expand_path(file, dir) }
.find { |file| File.executable?(file) }
if pnpm
warn 'Deprecated: bin/yarn now proxies to pnpm. Use bin/pnpm.' unless ENV['PNPM_PROXY_SILENT']
exec pnpm, *ARGV
else
$stderr.puts 'pnpm executable was not detected in the system.'
$stderr.puts 'Install pnpm via https://pnpm.io/installation'
exit 1
end
end
  1. Call pnpm from Rails scripts:
# bin/setup (snippet)
system! 'bin/pnpm', 'install', '--frozen-lockfile'
# bin/update (snippet)
# system('bin/pnpm', 'install', '--frozen-lockfile')
  1. Webpacker binaries (if you still use webpacker):
# Add to both bin/webpack and bin/webpack-dev-server
ENV['WEBPACKER_NODE_MODULES_BIN_PATH'] ||= File.expand_path('../node_modules/.bin', __dir__)
  1. Capistrano: use pnpm during deploys.

Create lib/capistrano/tasks/pnpm.rake:

# frozen_string_literal: true
namespace :pnpm do
desc 'Install JavaScript dependencies using pnpm'
task :install do
on roles(fetch(:pnpm_roles, :web)) do
within release_path do
with fetch(:pnpm_env, {}) do
flags = Array(fetch(:pnpm_flags, %w[--frozen-lockfile]))
execute :pnpm, :install, *flags
end
end
end
end
end
before 'deploy:updated', 'pnpm:install'

And in config/deploy.rb:

# Ensure node_modules is NOT a linked dir with pnpm
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'storage'
# Important: pass strict install flags to pnpm
set :pnpm_flags, %w(--frozen-lockfile)
  1. (Optional, webpacker only) Replace the yarn check with a pnpm check:
# lib/tasks/pnpm.rake
namespace :webpacker do
begin
Rake::Task['webpacker:check_yarn'].clear
rescue RuntimeError, NameError
end
desc 'Verifies if pnpm is installed (replaces webpacker yarn check)'
task :check_yarn do
pnpm_version = `pnpm --version`.strip
if pnpm_version.empty?
$stderr.puts 'pnpm not installed. Please install pnpm to compile webpacker assets.'
exit!
end
end
end

How to change CI/CD (optional)

Keep this for last. After local migration is green, update pipelines.

GitHub Actions

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- run: corepack enable && corepack prepare pnpm@latest --activate
- run: pnpm install --frozen-lockfile
- run: bin/rails assets:precompile

GitLab CI

cache:
- key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store/
before_script:
- |
if command -v corepack >/dev/null; then
corepack enable
corepack prepare pnpm@latest --activate
else
npm i -g pnpm@latest
fi
install:
script:
- pnpm install --frozen-lockfile --store-dir .pnpm-store

CircleCI

- node/install-packages:
pkg-manager: pnpm
cache-key: pnpm-lock.yaml

Conclusion

Enjoy faster, stricter, and much leaner pnpm!

My 2025 Monthly Subscription Review List

Permalink

Like the previous years 2022 / 2023 / 2024, here is my current subscription list. The first number is RMB per month.

  1. (68) iCloud 2T
  2. (17) Apple Music family plan
  3. (26) Dragonruby Pro (annual 42 USD)
  4. (30) Bandwagon host (monthly 33 USD, shared)
  5. (6) Adblock Pro (annual 70 RMB)
  6. (6.5) Blog domain (annual 11 USD)
  7. (1.5) 香哈菜谱 (annual 18 RMB)
  8. (49) AWS hosting (3 years 206 USD, monthly 1.35 USD)
  9. (16.8) Meituan biking (monthly)
  10. (47) Google Workspace Business Starter (yearly 80 USD)
  11. (115) Cursor.sh AI editor. (yearly 192 USD)
  12. (8.2) Ivory for Mastodon (yearly 98 RMB)
  13. (8) IndieWeb.Social Backer. (monthly 1.5 SGD)
  14. (31) Sublime Text and Merge (3 years, 152 AUD)
  15. (21) Surge (yearly, 46 USD)
  16. (87) RORvsWild (monthly, 10 EUR)
  17. (156) ChatGPT (monthly, 22 USD)

So totally 694 RMB per month to pay. In the previous year it was 480 RMB, so 45% higher compared to 2024.

Install Oracle Instant Client 23.3 - the First macOS ARM64 Driver and Ruby-oci8 Gem

Permalink

Prerequisite

  • Command line tools for Xcode or Xcode (by executing xcode-select --install) or [Xcode]

Download Oracle Instant Client Packages

Go oracle site and download:

  • instantclient-basiclite-macos.arm64-23.3.0.23.09.dmg
  • instantclient-sdk-macos.arm64-23.3.0.23.09.dmg
  • instantclient-sqlplus-macos.arm64-23.3.0.23.09.dmg

Mount DMG package and prepare folder

install basiclite
hdiutil mount ~/Downloads/instantclient-basiclite-macos.arm64-23.3.0.23.09.dmg
cd /Volumes/instantclient-basiclite-macos.arm64-23.3.0.23.09
sh ./install_ic.sh
install sdk
hdiutil mount ~/Downloads/instantclient-sdk-macos.arm64-23.3.0.23.09.dmg
cd /Volumes/instantclient-sdk-macos.arm64-23.3.0.23.09
sh ./install_ic.sh
install sqlplus
hdiutil mount ~/Downloads/instantclient-sqlplus-macos.arm64-23.3.0.23.09.dmg
cd /Volumes/instantclient-sqlplus-macos.arm64-23.3.0.23.09
sh ./install_ic.sh
move to opt folder
sudo mv ~/Downloads/instantclient_23_3 /opt

Install Oracle Instant Client

install ruby-oci8
cd /usr/local/bin
sudo ln -s /opt/instantclient_23_3/sqlplus sqlplus
export OCI_DIR=/opt/instantclient_23_3
gem install ruby-oci8

Put tnsnames.ora

setting TNS_ADMIN
export TNS_ADMIN=/opt/instantclient_23_3/network/admin/

Expose Local Server to the Internet Without Ngrok

Permalink

Option 1

config/environments/development.rb
Rails.application.configure do
config.hosts << ".example.com"
end
config/puma.rb
ssl_bind "0.0.0.0""8443"{
# Run "certbot certificates" to get these
key "server.key".
cert"server.crt"
}
expose SSH port:
ssh -R 8443:localhost:8443 -N user@example.com

Option 2

ssh -C2qTnN -R 3003:localhost:3003 user@ssss.com
# Then, configure an Nginx proxy with a 3003 domain name for external access

Option 3

Open cursor, goto port tab to create one.

If meet error goto '/Applications/Cursor.app/Contents/Resources/app/bin' and rename cursor-tunnel to code-tunnel.

Sponge Command Cheetsheet

Permalink

go build -o $(which sponge) cmd/sponge/main.go
go build -o $(which protoc-gen-go-gin) cmd/protoc-gen-go-gin/main.go
go build -o $(which protoc-gen-go-rpc-tmpl) cmd/protoc-gen-go-rpc-tmpl/main.go
go build -o $(which protoc-gen-json-field) cmd/protoc-gen-json-field/main.go

Install a New Rails App in Rocky Linux 9.6 at Aliyun

Permalink

Disable SELinux

sudo vi /etc/selinux/config
grubby --update-kernel ALL --args selinux=0

Install htop and atop

sudo dnf update
sudo dnf install epel-release
sudo dnf install htop
sudo dnf install atop

Install nginx

sudo dnf install nginx

Install node.js v22

Using nodesource distribution

curl -fsSL https://rpm.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo yum install -y nodejs
sudo yum groupinstall 'Development Tools'

Install yarn

curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
sudo yum install yarn

Install rbenv and ruby-build

whoami # should run as a ecs-user
git clone https://git.thape.com.cn/rails/rbenv.git .rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
~/.rbenv/bin/rbenv init # also edit ~/.bash_profile
# As an rbenv plugin
mkdir -p "$(rbenv root)"/plugins
git clone https://git.thape.com.cn/rails/ruby-build.git "$(rbenv root)"/plugins/ruby-build
git clone https://git.thape.com.cn/rails/rbenv-china-mirror.git "$(rbenv root)"/plugins/rbenv-china-mirror

Install Ruby 3.2.8

Ruby 3.2.8 need Rust to build JIT.

sudo dnf config-manager --enable crb
sudo dnf install libffi-devel libyaml-devel readline-devel
sudo dnf install zlib-devel openssl-devel gdbm-devel ncurses-devel bzip2-devel
sudo yum install -y rust # version 1.84.1
rbenv install -l
rbenv install 3.2.8
rbenv global 3.2.8
rbenv shell 3.2.8
echo "gem: --no-document" > ~/.gemrc
gem update --system

Prepare the capistrano deploy folder

whoami # should run as a ecs-user
cd /var/www
sudo mkdir harman
sudo chown ecs-user:ecs-user harman/

Add Credentials from the source site

vi .netrc
machine cnb.cool login cnb password asdasdas

Running IRuby Notebook in Cursor

Permalink

brew install ruby
brew install python3
brew install zeromq # require by ipykernel
/opt/homebrew/bin/python3 -m pip install ipykernel -U --user --force-reinstall --break-system-packages
gem install iruby
gem install rubygems-requirements-system
iruby register --force

Also install Jupyter in cursor.

To make mlx-engine running:

python3 -m pip install sentencepiece -U --user --force-reinstall --break-system-packages
python3 -m pip install outlines -U --user --force-reinstall --break-system-packages
python3 -m pip install outlines==1.1.1 --user --force-reinstall --ignore-requires-python --break-system-packages
python3 -m pip install datasets==3.6.0 --user --force-reinstall --ignore-requires-python --break-system-packages
python3 -m pip install mlx_vlm -U --user --force-reinstall --break-system-packages
python3 -m pip install mlx-lm -U --user --force-reinstall --break-system-packages

Install Open Project V16 in a Rocky Linux 9

Permalink

I already install an open project instance one and half year ago, but it's retired to sync with the production server OS version, which is Rocky Linux 8.10. After Ruby 3.4 released, I found the nokogiri v1.18 version and grpc v1.71 version both need a new GLIBC_2.29 version which is unavailable in Rocky Linux 8 series.

So I decide install a new Open Project instance server for the new production.

Disable SELinux

vi /etc/selinux/config
grubby --update-kernel ALL --args selinux=0

Install htop and atop

sudo dnf update
sudo dnf install epel-release
sudo dnf install htop
sudo dnf install atop

Install nginx

sudo dnf install nginx

Install node.js v22

Using nodesource distribution

curl -fsSL https://rpm.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo yum install -y nodejs
sudo yum groupinstall 'Development Tools'

Install yarn

curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
sudo yum install yarn

Install postgresql 16 client

Following DO manual

dnf module list postgresql
sudo dnf module enable postgresql:16
sudo dnf install postgresql-devel glibc-all-langpacks
sudo dnf install postgresql-contrib # pg_trgm btree_gist require by open project
sudo dnf install mysql-devel # if need to link to mysql server

Setup open_project user account

adduser open_project
cd /etc/sudoers.d/
echo "open_project ALL=(ALL) NOPASSWD:ALL" > 30-open_project-user
sudo su - open_project
mkdir .ssh
chmod 700 .ssh
vi .ssh/authorized_keys # and paste your public key
chmod 600 .ssh/authorized_keys

Install rbenv and ruby-build

whoami # should run as a open_project
git clone https://git.thape.com.cn/rails/rbenv.git .rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
~/.rbenv/bin/rbenv init # also edit ~/.bash_profile
# As an rbenv plugin
mkdir -p "$(rbenv root)"/plugins
git clone https://git.thape.com.cn/rails/ruby-build.git "$(rbenv root)"/plugins/ruby-build
git clone https://git.thape.com.cn/rails/rbenv-china-mirror.git "$(rbenv root)"/plugins/rbenv-china-mirror

Install Ruby 3.3.8

Ruby 3.3.8 need Rust to build JIT.

dnf config-manager --enable crb
dnf install libyaml-devel
yum install -y rust # version 1.79.0
dnf install clang-devel # for some gem like autocorrect-rb
rbenv install -l
rbenv install 3.3.8
rbenv global 3.3.8
rbenv shell 3.3.8
echo "gem: --no-document" > ~/.gemrc
gem update --system

Prepare the capistrano deploy folder

whoami # should run as a open_project
cd /var/www
sudo mkdir open_project
sudo chown open_project:open_project open_project/

Using mirror when deploy

Run in the release rails root folder

bundle config mirror.https://rubygems.org https://gems.ruby-china.com

Setting the open project settings

/etc/environment
OPENPROJECT_EDITION=bim
OPENPROJECT_APP__TITLE=天华项目全生命周期管理
OPENPROJECT_APP__SHORT__TITLE=PLM
OPENPROJECT_HOST__NAME=plm-staging.thape.com.cn
OPENPROJECT_EMAIL__DELIVERY__METHOD="smtp"
OPENPROJECT_SMTP__ADDRESS="smtp.thape.com.cn"
OPENPROJECT_SMTP__PORT="25"
OPENPROJECT_SMTP__DOMAIN="thape.com.cn"
OPENPROJECT_SMTP__AUTHENTICATION="login"
OPENPROJECT_SMTP__USER__NAME="plm"
OPENPROJECT_SMTP__PASSWORD=""
OPENPROJECT_SMTP__ENABLE__STARTTLS__AUTO="true"
OPENPROJECT_SMTP__OPENSSL__VERIFY__MODE="none"
OPENPROJECT_ENTERPRISE__TRIAL__CREATION__HOST="https://www.google-analytics.com"
GRUF_OP_SERVER="172.17.1.1:10009"
WX_TEMPLATE_ID=""
WX_WORK_PACKAGE_DETAIL="https://plm.thape.com.cn/work_packages/:id"
MP_QRCODE_ABS_PATH="/var/www/open_project/shared/public/static/mp_qrcode.jpg"
LOGO_ABS_PATH="/var/www/open_project/shared/public/static/logo_plm.png"
CSP_FRAME_SRC="https://ith-workspace.thape.com.cn"
CSP_CONNECT_SRC="https://analytics.thape.com.cn"
WECHAT_AUTH_JWT_SECERT=""
WECHAT_AUTH_ITH_URL="/ith/wechat/ppm/login"
/etc/systemd/system/puma_plm.service
[Unit]
Description=Puma HTTP Server for open_project (staging)
After=syslog.target network.target
[Service]
Type=simple
WatchdogSec=10
User=open_project
EnvironmentFile=/etc/environment
WorkingDirectory=/var/www/open_project/current
ExecStart=/home/open_project/.rbenv/bin/rbenv exec bundle exec puma -e production
ExecReload=/bin/kill -SIGUSR1 $MAINPID
# if we crash, restart
RestartSec=10
Restart=on-failure
StandardOutput=append:/var/www/open_project/shared/log/puma.log
StandardError=append:/var/www/open_project/shared/log/puma.log
SyslogIdentifier=puma_plm
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
bundle exec rake openproject:plugins:register_frontend
bundle exec rake i18n:js:export
bundle exec rake db:seed
sudo journalctl -u puma_plm # check system log and fix errors
sudo systemctl start puma_plm

Upload custom fonts

/var/www/open_project/shared/public
gzip -9r op_public_files.zip fonts/ static/ WW_verify_*.txt

Change IP

Rocky 8 network change:

vi /etc/sysconfig/network-scripts/ifcfg-ens192

Rocky 9 network change:

vi /etc/NetworkManager/system-connections/ens192.nmconnection
nmcli connection reload /etc/NetworkManager/system-connections/ens192.nmconnection
nmcli connection up /etc/NetworkManager/system-connections/ens192.nmconnection

Install the dependency

sudo yum install ImageMagick

Open the firewall

sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
sudo firewall-cmd --reload

Migrate DB From Redis 7.4 to 7.2 and Skip the RDB Version 12 Error

Permalink

After I copy redis dump.rdb from redis 7.4 to my Macbook homebrew redis 7.2 I got Can't handle RDB format version 12 error:

# /opt/homebrew/opt/redis/bin/redis-server /opt/homebrew/etc/redis.conf
20066:C 17 Apr 2025 10:39:34.935 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
20066:C 17 Apr 2025 10:39:34.935 * Redis version=7.2.7, bits=64, commit=00000000, modified=0, pid=20066, just started
20066:C 17 Apr 2025 10:39:34.935 * Configuration loaded
20066:M 17 Apr 2025 10:39:34.935 * Increased maximum number of open files to 10032 (it was originally set to 256).
20066:M 17 Apr 2025 10:39:34.935 * monotonic clock: POSIX clock_gettime
Redis 7.2.7 (00000000/0) 64 bit
Running in standalone mode
Port: 6379
PID: 20066
https://redis.io
20066:M 17 Apr 2025 10:39:34.936 # WARNING: The TCP backlog setting of 511 cannot be enforced because kern.ipc.somaxconn is set to the lower value of 128.
20066:M 17 Apr 2025 10:39:34.936 * Server initialized
20066:M 17 Apr 2025 10:39:34.936 # Can't handle RDB format version 12
20066:M 17 Apr 2025 10:39:34.936 # Fatal error loading the DB, check server logs. Exiting.

Two option available, upgrade redis version via redis.io

Using high version

brew tap redis/redis
brew install --cask redis

The solution is ugly as redis will give you rc version of redis and it depend on the LLVM@18, which takes 1.7GB...

Stay redis 7.2

redis-cli shutdown
brew uninstall redis
brew untap redis/redis
# make sure your DB no need!
rm /opt/homebrew/var/db/redis/dump.rdb
rm /opt/homebrew/etc/redis-sentinel.conf
rm /opt/homebrew/etc/redis.conf
rm /opt/homebrew/etc/redis.conf.default
brew install redis
brew services start redis

Using redis-dump to dump and restore DB.

redis-dump -u 127.0.0.1:6379 -d 0 > db_db0.json
cat db_db0.json | redis-load -d 0