株式会社ノベルワークス
home

TOP

keyboard_arrow_right

ブログ一覧

keyboard_arrow_right

kintoneアプリの更新完了を"自動で"検知して、バージョン管理を"無意識で"やりたい

ふぁーびー

2025/12/12

kintoneアプリの更新完了を"自動で"検知して、バージョン管理を"無意識で"やりたい

注意

この記事で紹介する手法は、kintoneの標準機能や公式なAPIとして公開されていない、非公式な手法となります。kintoneのアップデートにより動作しなくなる可能性があります。


はじめに

私のチームでは日頃の業務をkintoneアプリで行っており、アプリの管理については「アプリ管理権限を複数人に開放し、気付いた人が改善する」 という運用をしています。
最初のうちは改善したらチャットツールやkintoneのどこかしらで変更内容を共有して、、という運用をしていましたが、アプリも人も増えてくると「いつの間にかこれ変わってるけど、いつ誰が、、?」ということが多々発生するようになりました。

そこで、変更管理アプリを作成し、「変更点を手動で記録する」という運用ルールでカバーしていました。しかし、これはあくまで 「人の意識」 に依存した運用です。

  • 「ちょっとフィールド動かしただけだから」と記録を後回しにして忘れる

  • 記録はしたものの、内容に抜け漏れや誤りがある

  • 記録作業自体が面倒

といった課題がまた出てきました。そこで「人の意識を介さず、バックグラウンドで勝手に(無意識に)変更が検知・記録される仕組み」 を作ることにしました。

本記事では「無意識に」をどう実現するかをメインに書きます。

どうするか

目指すゴールは以下の通りです。

  1. ユーザーはいつも通り kintone の画面で「アプリを更新」ボタンを押すだけ。

  2. 更新が完了した瞬間、システムがそれを検知する。

  3. 「変更後の設計情報」 が自動的に構成管理アプリに登録される。

「アプリの更新完了」をどう検知するか

アプリの更新完了については、kintone JavaScript API イベントやWebhookでは提供されていません。
またアプリの設定画面にはJavaScriptカスタマイズを適用することはできません。
そこで今回はGoogle Chromeの拡張機能であるTampermonkeyを利用し、オリジナルのJavaScriptを適用させます。

次に何をもって「アプリ更新完了」とみなすかですが、単に「アプリを更新」ボタンのクリックイベント (click) を拾うだけでは、クリック後にバリデーションエラーで更新が失敗する可能性などが考えられます。

DevToolsで通信を解析した結果、以下の流れで内部APIによる処理が行われていることが分かりました。

  1. deploy.json (POST): 更新予約(ここにはAppIDが含まれる)。

  2. getDeployProgress.json (POST): 進捗確認(ポーリングされ、完了すると status: "SUCCESS" が返る)。

そこで XMLHttpRequest をモンキーパッチし、deploy.json で対象アプリを特定」し、「getDeployProgress.json で完了を見届ける」 というロジックを組みました。

実装のポイント

Geminiに生成させたサンプルコードは以下の通りです。
(先述のTampermonkeyで適用し動作することは確認しておりますが、ご利用に際しては自己責任でお願いします)

(function() {
    console.log("%c[AutoVC] 監視を開始しました", "color: white; background: #008080; padding: 4px; border-radius: 4px;");

    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    // 状態保持用
    let pendingAppId = null; // 更新対象のアプリID
    let pollCount = 0;       // ポーリング回数のカウント

    // 1. open: URLを記録
    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url;
        this._method = method;
        return originalOpen.apply(this, arguments);
    };

    // 2. send: 通信内容(body)を捕捉し、レスポンスを監視
    XMLHttpRequest.prototype.send = function(body) {

        // send実行時の `body` (送信データ) はここで確保される
        const payloadData = body;

        this.addEventListener('load', function() {
            try {
                if (!this.responseText) return;

                // URL判定
                const isDeploy = this._url && this._url.includes('deploy.json');
                const isProgress = this._url && this._url.includes('getDeployProgress.json');

                // ---------------------------------------------------
                // A. デプロイ開始 (deploy.json)
                // ---------------------------------------------------
                if (isDeploy && this._method.toUpperCase() === 'POST' && this.status === 200) {

                    // 1. レスポンスで成功を確認 {"success": true}
                    const res = JSON.parse(this.responseText);
                    if (!res.success) {
                        console.warn("[AutoVC] deploy.jsonが呼ばれましたが success:true ではありません");
                        return;
                    }

                    // 2. リクエストペイロード(送信データ)からアプリIDを取得
                    // 形式: {"app": "1", "__REQUEST_TOKEN__": "..."}
                    if (payloadData) {
                        const req = JSON.parse(payloadData);

                        // アプリIDを取得(念のため数値型でも文字列型でも対応)
                        if (req.app) {
                            pendingAppId = String(req.app);
                            pollCount = 0;

                            console.log(`%c[AutoVC] デプロイ開始を検知 (AppID: ${pendingAppId})`, "color: white; background: green; font-weight: bold; padding: 2px 5px;");
                            console.log("Request Payload:", req);
                        }
                    }
                    return; // 処理終了
                }

                // ---------------------------------------------------
                // B. 進捗確認 (getDeployProgress.json)
                // ---------------------------------------------------
                if (isProgress && this.status === 200) {

                    // 【早期リターン】自分が開始したデプロイでなければ、処理せず終了
                    if (!pendingAppId) {
                        return;
                    }

                    // カウントアップ
                    pollCount++;

                    // グループ化してログ出力
                    console.groupCollapsed(`[AutoVC] 進捗確認 #${pollCount} (Target: ${pendingAppId})`);

                    const res = JSON.parse(this.responseText);
                    console.log("Response Body:", res);

                    // JSON構造チェック (res.result.item)
                    if (!res.result || !res.result.item) {
                        console.warn("想定外のJSON構造 (result.itemなし) -> スキップ");
                        console.groupEnd();
                        return;
                    }

                    const item = res.result.item;
                    const currentStatus = item.status;
                    const resAppId = String(item.appId);

                    console.log(`Status: %c${currentStatus}`, "font-weight: bold; color: #d35400;", `(ResAppID: ${resAppId})`);

                    // アプリIDの一致確認(念の為)
                    if (resAppId !== pendingAppId) {
                        console.warn(`ID不一致のため無視します (Target: ${pendingAppId} vs Res: ${resAppId})`);
                        console.groupEnd();
                        return;
                    }

                    // 成功判定 (SUCCESS または COMPLETED)
                    const isSuccess = (currentStatus === 'SUCCESS' || currentStatus === 'COMPLETED');

                    if (isSuccess) {
                        console.log("%cデプロイ完了!", "color: blue; font-weight: bold; font-size: 1.2em;");

                        alert(`【更新完了】\nアプリ(ID:${pendingAppId})の反映が完了しました!\n(確認回数: ${pollCount}, Rev: ${item.revision})`);

                        // 完了処理
                        pendingAppId = null;
                        pollCount = 0;

                    } else if (currentStatus === 'FAIL' || currentStatus === 'CANCELLED') {
                        console.error("デプロイ失敗またはキャンセルされました。");
                        pendingAppId = null;
                        pollCount = 0;
                    } else {
                        console.log("まだ処理中です...");
                    }

                    console.groupEnd();
                }

            } catch(e) {
                console.error("[AutoVC] エラー発生:", e);
                // グループが開いていれば閉じる安全策
                try { console.groupEnd(); } catch(err){}
            }
        });
        return originalSend.apply(this, arguments);
    };
})();

重要なポイントだけピックアップします。

通信の監視

kintone内部の通信を捕捉するため、ブラウザのネイティブオブジェクトである XMLHttpRequest をオーバーライド(Monkey Patch)しています。

  • prototype.open のラップ:

  • リクエスト先のURL(エンドポイント)をインスタンスプロパティ _url として保持し、後続の処理で判定に使用します。

  • prototype.send のラップ:

  • リクエストボディ(Payload)を捕捉し、変種に保存(deploy.json 実行時のApp ID特定に使用)。

  • load イベントリスナーを注入し、サーバーからのレスポンス(ステータスコードやレスポンスボディ)を非同期で傍受します。

"更新完了"の検知

kintoneのアプリ更新プロセスは「アプリ更新をリクエスト」して「更新完了まで待つ」という流れになります。
REST APIでもアプリの設定を運用環境へ反映するAPIアプリの設定の運用環境への反映状況を確認するAPIがわかれているので、画面での操作も同じような感じですね。

※このあたりのことは過去に記事を書いているのでよければご覧ください

kintone REST APIでアプリ間連携(ルックアップ/関連レコード/アクション)を含むアプリを作成する #kintoneRestApi - Qiita

deploy.jsonへのリクエスト → アプリの設定を運用環境へ反映するAPI
getDeployProgress.json → アプリの設定の運用環境への反映状況を確認するAPI

という対応になっているようなので、この2つへの通信のペイロードおよびレスポンスを見て、最終的には反映状況が「SUCCESS」となることを待ち、「更新完了」とみなしています。


ここまでできれば、後は更新完了時に各種設計情報をAPIで取得してkintoneへ(ここはなんでもいいですが)貯めていくことで、

  • いつ誰がどこをどう変えたのかの確認

  • 最新バージョンとある時点のバージョンの差分は何かの確認

  • Webhook等用いて危険な変更が発生していないかを検知

といったデータの活用もでき、また「変更の目的や背景」も入力させて設計内容とセットで貯めておくことができれば、より管理がしやすくなりそうです。
(AIに食わせてナレッジ収集など、、面白いこともできそうですね、、👀)


今後の発展と課題

現状の仕組みは、アプリ更新を行う全ユーザーがこのスクリプトを実行していないと、監視の目をすり抜けて更新が可能です。
これを防ぐためには、このスクリプトが実行されていないとkintoneを使えないといった仕組みを全体カスタマイズと組み合わせることによって実現するなど、検討する必要がありそうです🤔

・・・と、ここまでハックしておいて何ですが、やはり公式の機能で実現できるのが一番です。

「アプリ更新完了」を検知できるイベントやWebhook が公式機能として実装されることを楽しみにしています🥳

余談

昨年はこんな記事を書いたんですが、(たったの)1年経った今ではバックエンドも含めてVibe Codingでサクッと作れるような事態になっており、流れの爆速さに息切れ気味です・・・今年もAIがらみのことを書こうと思いましたが、kintoneをハックしてみました。(結局コードはAIが書いてますが)
来年もよろしくお願いします。