Skip to content

fix(prime-checker): correct Mersenne prime facts across all 17 locales #207

fix(prime-checker): correct Mersenne prime facts across all 17 locales

fix(prime-checker): correct Mersenne prime facts across all 17 locales #207

Workflow file for this run

name: Deploy to VPS
on:
push:
branches: [main]
workflow_dispatch: {}
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 40
needs: []
defaults:
run:
working-directory: site
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
steps:
- uses: actions/checkout@v4
# 218ツール×17言語=約7,800ページのSSGでランナーのディスクが枯渇する(ENOSPC)。
# ビルド前に未使用のプリインストールSDKを削除して約30GB確保する。
- name: Free disk space
working-directory: /
run: |
echo "Before:"; df -h / | tail -1
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup \
/usr/local/lib/android /usr/local/share/boost \
/usr/local/share/powershell /usr/share/swift "$AGENT_TOOLSDIRECTORY" || true
sudo docker image prune -af || true
echo "After:"; df -h / | tail -1
- uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
cache-dependency-path: site/package-lock.json
- name: Install dependencies
run: npm ci --no-fund --no-audit
- name: Type check
run: npm run typecheck
- name: i18n / SEO audits
run: |
npm run audit:i18n
npm run audit:seo
- name: Unit tests
run: npm test
- name: Production build (standalone)
run: npm run build
env:
NEXT_PUBLIC_SITE_URL: https://toolify365.com
NEXT_PUBLIC_ORG_NAME: Toolify
NEXT_PUBLIC_CONTACT_EMAIL: app.develop.sk@gmail.com
NEXT_PUBLIC_ADSENSE_CLIENT: ca-pub-4927026308242118
NEXT_PUBLIC_FC_ID: ""
NEXT_PUBLIC_BING_SITE_VERIFICATION: ${{ secrets.BING_SITE_VERIFICATION }}
NEXT_PUBLIC_YANDEX_SITE_VERIFICATION: ${{ secrets.YANDEX_SITE_VERIFICATION }}
# AdSense ad unit slot IDs (set after AdSense approval; empty = placeholder shown)
NEXT_PUBLIC_ADSENSE_SLOT_BANNER: ${{ secrets.ADSENSE_SLOT_BANNER }}
NEXT_PUBLIC_ADSENSE_SLOT_INARTICLE: ${{ secrets.ADSENSE_SLOT_INARTICLE }}
NEXT_PUBLIC_ADSENSE_SLOT_STICKY: ${{ secrets.ADSENSE_SLOT_STICKY }}
NEXT_PUBLIC_ADSENSE_SLOT_BELOWRESULT: ${{ secrets.ADSENSE_SLOT_BELOWRESULT }}
NEXT_PUBLIC_ADSENSE_SLOT_INFEED: ${{ secrets.ADSENSE_SLOT_INFEED }}
NEXT_PUBLIC_ADSENSE_SLOT_ANCHOR: ${{ secrets.ADSENSE_SLOT_ANCHOR }}
NEXT_PUBLIC_GA_ID: ${{ secrets.GA_ID }}
NEXT_PUBLIC_CLARITY_PROJECT_ID: ${{ secrets.CLARITY_PROJECT_ID }}
# 楽天市場 価格比較ツール (/api/price-search) — サーバーサイドのみ
RAKUTEN_APP_ID: ${{ secrets.RAKUTEN_APP_ID }}
RAKUTEN_ACCESS_KEY: ${{ secrets.RAKUTEN_ACCESS_KEY }}
RAKUTEN_AFFILIATE_ID: ${{ secrets.RAKUTEN_AFFILIATE_ID }}
# 楽天アプリ登録のApplication URLと一致させる
RAKUTEN_ORIGIN_URL: ${{ secrets.RAKUTEN_ORIGIN_URL }}
# Amazon アソシエイト (承認後に設定)
NEXT_PUBLIC_AMAZON_PARTNER_TAG: ${{ secrets.AMAZON_PARTNER_TAG }}
- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
- name: Write runtime env file for VPS
env:
RAKUTEN_APP_ID: ${{ secrets.RAKUTEN_APP_ID }}
RAKUTEN_ACCESS_KEY: ${{ secrets.RAKUTEN_ACCESS_KEY }}
RAKUTEN_AFFILIATE_ID: ${{ secrets.RAKUTEN_AFFILIATE_ID }}
RAKUTEN_ORIGIN_URL: ${{ secrets.RAKUTEN_ORIGIN_URL }}
run: |
printf 'RAKUTEN_APP_ID=%s\nRAKUTEN_ACCESS_KEY=%s\nRAKUTEN_AFFILIATE_ID=%s\nRAKUTEN_ORIGIN_URL=%s\n' \
"$RAKUTEN_APP_ID" "$RAKUTEN_ACCESS_KEY" "$RAKUTEN_AFFILIATE_ID" "$RAKUTEN_ORIGIN_URL" \
> .next/standalone/.env.local
- name: rsync standalone artifacts to VPS
run: |
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new" \
.next/standalone/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new" \
.next/static/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/.next/static/"
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new" \
public/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/public/"
- name: Clear ISR / full-route cache on VPS
run: |
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \
"$DEPLOY_USER@$DEPLOY_HOST" \
"rm -rf \"$DEPLOY_PATH/.next/cache/\" && echo 'cache cleared'"
- name: Reload toolify on VPS via PM2 (zero-downtime)
run: |
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \
"$DEPLOY_USER@$DEPLOY_HOST" \
"pm2 reload toolify --update-env && pm2 save"
- name: Smoke test live URL
run: |
set -e
for i in 1 2 3 4 5; do
if curl -sSfL -o /dev/null -w "%{http_code}" --max-time 15 https://toolify365.com/en/tools/bmi-calculator | grep -q 200 && curl -sSf --max-time 10 https://toolify365.com/feed-ja.xml | grep -q "<rss"; then
echo "✓ Live URL returning 200"
exit 0
fi
echo "Attempt $i failed, retrying in 5s..."
sleep 5
done
echo "✗ Live URL not returning 200 after 5 attempts"
exit 1
# Pinterest RSS自動投稿の回帰ガード(2026-07-21の実障害):
# feedのenclosure画像が全item同一になると、Pinterestの画像ハッシュ重複排除で
# ピン化が2枚で無言停止する(/api/og がクエリを読まなくなる等の回帰で再発しうる)。
# 先頭3itemの画像を実取得しMD5が2種以上あることを検証。失敗=workflow赤で
# 朝のCI監視ルーチンが検知する。
- name: Guard feed pin images are unique (Pinterest dedupe)
run: |
set -e
urls=$(curl -sSf --max-time 15 https://toolify365.com/feed.xml \
| grep -o 'enclosure url="[^"]*"' | sed 's/enclosure url="//;s/"$//;s/\&amp;/\&/g' | head -3)
[ -n "$urls" ] || { echo "✗ feedにenclosureが無い"; exit 1; }
md5s=""
while IFS= read -r u; do
curl -sSf --max-time 30 "$u" -o /tmp/pin.png
bytes=$(wc -c < /tmp/pin.png | tr -d ' ')
h=$(md5sum /tmp/pin.png | cut -d' ' -f1)
echo " $h ${bytes}B $u"
# 2026-08-14の実障害: /api/og が edge ランタイムでフォントを読めず、
# 全リクエストが 70B の 1×1 透明PNG フォールバックに落ちていた。
# 「200が返る」だけでは検知できないのでバイト数で門番する。
if [ "$bytes" -lt 20000 ]; then
echo "✗ ピン画像が ${bytes}B しかない → /api/og が空白PNGフォールバックに落ちている"
exit 1
fi
md5s="$md5s$h\n"
done <<< "$urls"
uniq_count=$(printf "%b" "$md5s" | sort -u | grep -c .)
if [ "$uniq_count" -lt 2 ]; then
echo "✗ feedのピン画像が全て同一(MD5 1種) → Pinterest重複排除でRSS投稿が止まる回帰"
exit 1
fi
echo "✓ ピン画像 unique ($uniq_count 種/3件)"
- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/deploy_key