/** * apply-to-site-488755-production.js * ------------------------------------------------------------ * 488755「【開発中】★建売マスターシート」の staging環境(nextoffice2.next-hd.net)で * 検証済みのSiteSettingsを、production環境(nextoffice.next-hd.co.jp)の同一SiteId 488755へ * 丸ごと適用する(改修4ステップ原則のステップ4: production反映)。 * * 前提確認(実行前、2026-08-31): * - production側488755は実データ1件のみ(テスト用の空レコード、最終更新2026-06-06)、 * 業務運用は実質されていない。SiteSettings丸ごと置き換えの実害は極めて小さいと判断。 * - Title/Permissions(4件)は staging/production で完全一致を確認済み。 * * 送信直前に staging/production 双方をAPIから直接再取得する(ローカルconfigsキャッシュの * 同期漏れによる巻き戻り事故を防ぐため。189112 CheckH復元時の教訓)。 * Title/ReferenceType/ParentId/InheritPermission/Permissions は production側の現状値を維持し、 * SiteSettings のみ staging側の内容で丸ごと置き換える。 * * 使い方: * node apply-to-site-488755-production.js … 差分表示・プレビュー保存のみ(送信なし) * node apply-to-site-488755-production.js --execute … 上記に加えて実際にproductionへ送信する * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { findSiteDir, newModifyDir } = require("../../site-paths"); const { REPO_ROOT, loadServerConfig } = require("../../resolve-project"); const PROJECT_NAME = "★マスターシート"; const SITE_ID = 488755; const SOURCE_ENV = "staging"; const TARGET_ENV = "production"; const baseDir = path.join(REPO_ROOT, PROJECT_NAME); const execute = process.argv.includes("--execute"); function maskApiKey(key) { if (!key) return key; return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****"; } function timestamp() { return new Date().toISOString().replace(/[:.]/g, "-"); } async function getSite(env, siteId) { const config = loadServerConfig(env); const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/getsite`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }), }); const json = await res.json(); if (json.StatusCode !== 200) { console.error(`[エラー] ${env}のgetsiteに失敗しました:`, JSON.stringify(json).slice(0, 500)); process.exit(1); } return { config, data: json.Response.Data }; } function diffTopLevel(before, after) { const lines = []; const afterKeys = Object.keys(after || {}); let changed = false; for (const key of afterKeys) { const b = JSON.stringify(before?.[key]); const a = JSON.stringify(after?.[key]); if (b !== a) { changed = true; const bLen = Array.isArray(before?.[key]) ? `(${before[key].length}件)` : ""; const aLen = Array.isArray(after?.[key]) ? `(${after[key].length}件)` : ""; lines.push(` [変更] SiteSettings.${key} ${bLen} → ${aLen}`); } } if (!changed) lines.push(" 差分なし"); return lines; } (async () => { console.log("========================================"); console.log(` 488755 production反映(staging → production)${execute ? "(実行)" : "(プレビューのみ)"}`); console.log("========================================"); console.log(`\n[INFO] ${SOURCE_ENV}環境からSiteId ${SITE_ID} を取得中...`); const { data: sourceData } = await getSite(SOURCE_ENV, SITE_ID); console.log(`[OK] 取得しました(Ver: ${sourceData.Ver}, UpdatedTime: ${sourceData.UpdatedTime})`); console.log(`\n[INFO] ${TARGET_ENV}環境からSiteId ${SITE_ID} を取得中...`); const { config: targetConfig, data: targetData } = await getSite(TARGET_ENV, SITE_ID); console.log(`[OK] 取得しました(Ver: ${targetData.Ver}, UpdatedTime: ${targetData.UpdatedTime})`); // Permissions・Title等の整合性チェック(想定外の食い違いがあれば警告し停止する) const titleMatch = sourceData.Title === targetData.Title; const permMatch = JSON.stringify(sourceData.Permissions) === JSON.stringify(targetData.Permissions); console.log(`\nTitle一致: ${titleMatch ? "OK" : "[警告] 不一致 staging=" + sourceData.Title + " / production=" + targetData.Title}`); console.log(`Permissions一致: ${permMatch ? "OK" : "[警告] 不一致"}`); const modifiedSiteSettings = sourceData.SiteSettings; const url = `${targetConfig.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`; const body = { ApiVersion: targetConfig.ApiVersion || "1.1", ApiKey: targetConfig.ApiKey, SiteId: SITE_ID, Title: targetData.Title, ReferenceType: targetData.ReferenceType, ParentId: targetData.ParentId, InheritPermission: targetData.InheritPermission, Permissions: targetData.Permissions, SiteSettings: modifiedSiteSettings, }; console.log(`\nURL : ${url}`); console.log(`Title : ${targetData.Title}(production現状のまま維持)`); console.log(`InheritPermission: ${targetData.InheritPermission}(production現状のまま維持)`); console.log(""); console.log("--- SiteSettings 差分(production現状 → staging由来の反映後) ---"); const diffLines = diffTopLevel(targetData.SiteSettings, modifiedSiteSettings); diffLines.forEach((l) => console.log(l)); console.log(""); console.log(`Columns件数 : ${(targetData.SiteSettings?.Columns || []).length} → ${modifiedSiteSettings.Columns.length}`); console.log(`Sections件数 : ${(targetData.SiteSettings?.Sections || []).length} → ${modifiedSiteSettings.Sections.length}`); console.log(`Scripts件数 : ${(targetData.SiteSettings?.Scripts || []).length} → ${modifiedSiteSettings.Scripts.length}`); console.log(`Processes件数: ${(targetData.SiteSettings?.Processes || []).length} → ${modifiedSiteSettings.Processes.length}`); console.log(`Links件数 : ${(targetData.SiteSettings?.Links || []).length} → ${modifiedSiteSettings.Links.length}`); const configsDir = path.join(baseDir, "configs", TARGET_ENV); const targetSiteDir = findSiteDir(configsDir, SITE_ID); const ts = timestamp(); if (targetSiteDir) { const modifyDir = newModifyDir(targetSiteDir, "staging反映_production"); fs.writeFileSync( path.join(modifyDir, `before_site-${SITE_ID}_production_${ts}.json`), JSON.stringify({ StatusCode: 200, Response: { Data: targetData } }, null, 2), "utf-8" ); const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) }; fs.writeFileSync( path.join(modifyDir, `site-${SITE_ID}_preview_${ts}.json`), JSON.stringify(maskedBody, null, 2), "utf-8" ); const diffText = [ `488755 production反映 差分プレビュー (${ts})`, `URL: ${url}`, `Title: ${targetData.Title}`, "", "--- SiteSettings差分 ---", ...diffLines, "", `Columns件数 : ${(targetData.SiteSettings?.Columns || []).length} → ${modifiedSiteSettings.Columns.length}`, `Sections件数 : ${(targetData.SiteSettings?.Sections || []).length} → ${modifiedSiteSettings.Sections.length}`, `Scripts件数 : ${(targetData.SiteSettings?.Scripts || []).length} → ${modifiedSiteSettings.Scripts.length}`, `Processes件数: ${(targetData.SiteSettings?.Processes || []).length} → ${modifiedSiteSettings.Processes.length}`, ].join("\n"); fs.writeFileSync(path.join(modifyDir, `site-${SITE_ID}_apply_diff_${ts}.txt`), diffText, "utf-8"); console.log(`\n[OK] 変更前スナップショット・プレビュー・差分を保存しました: ${modifyDir}`); } if (!execute) { console.log("\n[注意] --execute が指定されていないため、送信は行っていません。"); console.log("内容を確認の上、問題なければ次を実行してください: node apply-to-site-488755-production.js --execute"); process.exit(0); } console.log("\n実際にproduction環境へ送信します(updatesite)..."); const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const text = await res.text(); console.log(`HTTP ${res.status} ${res.statusText}`); console.log(text); if (targetSiteDir) { const modifyDir = newModifyDir(targetSiteDir, "staging反映_production"); fs.writeFileSync(path.join(modifyDir, `site-${SITE_ID}_apply_result_${ts}.json`), text, "utf-8"); } })();