windmillワークフローの管理手順の変更仕様

This commit is contained in:
Akira
2026-03-03 14:39:57 +09:00
parent d5bb7f24dd
commit f771e6bcf7
2 changed files with 344 additions and 4 deletions

View File

@@ -0,0 +1,301 @@
# マスタードキュメント - Windmillフロー管理 API一本化編
> **最終更新**: 2026-03-03
> **対象**: `windmill.keinafarm.net` / workspace `admins`
> **目的**: ローカルGitとサーバーGitの衝突を避けつつ、Windmill APIを唯一の運用経路に統一する
---
## 目次
1. [この文書の役割](#1-この文書の役割)
2. [運用方針(結論)](#2-運用方針結論)
3. [現状の課題と解決方針](#3-現状の課題と解決方針)
4. [管理対象と正本の定義](#4-管理対象と正本の定義)
5. [同期・反映の仕様](#5-同期反映の仕様)
6. [競合時の動作仕様](#6-競合時の動作仕様)
7. [実装計画](#7-実装計画)
8. [標準運用手順Runbook](#8-標準運用手順runbook)
9. [セキュリティ・監査方針](#9-セキュリティ監査方針)
10. [障害前提の復旧設計(必須)](#10-障害前提の復旧設計必須)
11. [Windmill依存を薄くする方針必須](#11-windmill依存を薄くする方針必須)
12. [受け入れ条件](#12-受け入れ条件)
13. [既知の注意点](#13-既知の注意点)
14. [更新履歴](#14-更新履歴)
---
## 1. この文書の役割
この文書は、次回セッション開始時にこれだけ読めば作業を継続できることを目的とした、**運用仕様 + 実装計画の単一ソース**である。
- 暗黙知を残さない
- 方針・手順・失敗時の扱いを固定化する
- API経由運用に必要な実装タスクを明文化する
---
## 2. 運用方針(結論)
### 採用する方式: API一本化Server First
1. ローカルリポジトリは、サーバー側Gitをリモートにしない
2. Windmillの実体変更は **Windmill REST API 経由のみ**
3. 作業開始時にAPIでサーバー状態を取り込み、サーバーが新しければローカルへ同期
4. ローカル変更はAPIでサーバーへ反映
5. サーバー側の Git Sync Workflow は定期記録用途として継続
6. ローカルGitコミットは手動またはAIで実施し、監査/履歴用途として扱う
---
## 3. 現状の課題と解決方針
### 課題
- Windmill側の自動Git記録WFと、ローカルGit運用が同一系統を触ると競合が発生する
- `wmill` CLIは制約があり使いにくく、運用が不安定
- サーバーとローカルのどちらが最新か判定が曖昧
### 解決
- 更新経路をWindmill APIに統一し、競合面を縮小する
- 「正本=Windmillサーバー」の原則を固定する
- `hash` / `parent_hash` を使った衝突検知を標準化する
---
## 4. 管理対象と正本の定義
### 正本
- **Windmillサーバー上のオブジェクト**
- scripts (`/w/{workspace}/scripts/*`)
- flows (`/w/{workspace}/flows/*`)
- schedules (`/w/{workspace}/schedules/*`)
### ローカルの位置づけ
- ローカルファイルは「編集用ワークツリー + 監査ログ」
- ローカルGitはサーバー同期の必須経路ではない
### 既存の主要オブジェクト(例)
- script: `u/admin/alexa_speak`
- flows: `f/app_custom/system_heartbeat`, `f/shiraou/shiraou_notification` など
---
## 5. 同期・反映の仕様
## 5.1 Pullサーバー -> ローカル)
### 目的
- 作業前にサーバー最新状態をローカルへ取り込む
### 判定ルール
- サーバー `updated_at` / `hash` とローカル管理メタ情報を比較
- サーバーが新しければローカルを更新
### 対象API
- `GET /api/w/{workspace}/scripts/get/p/{path}`
- `GET /api/w/{workspace}/flows/get/{path}`
- `GET /api/w/{workspace}/schedules/get/{path}`(必要時)
## 5.2 Pushローカル -> サーバー)
### 目的
- ローカル変更をサーバーへ安全反映
### スクリプト反映API標準
- `POST /api/w/{workspace}/scripts/create`
必須パラメータ(最小):
- `path`
- `parent_hash`直前取得したサーバーhash
- `summary`
- `description`
- `content`
- `schema`
- `language`TypeScriptは `bun`
- `kind`(通常 `script`
- `lock`(既存値継承)
### フロー反映API
- CE制約により `PUT` 更新が不可/不安定な場合があるため、原則:
1. `DELETE /flows/delete/{path}`
2. `POST /flows/create`
---
## 6. 競合時の動作仕様
### スクリプトPush競合
- `parent_hash` 不一致時は更新拒否(期待動作)
- 対応:
1. 最新をPull
2. 差分を再適用
3. 再Push
### フローPush競合
- 削除再作成前に最新を必ずPullしてローカル保存
- 競合が疑われる場合は自動実行せず手動確認にフォールバック
---
## 7. 実装計画
## Phase 0: 文書固定(完了)
- 本ドキュメント作成
## Phase 1: API運用コマンド整備次タスク
`wm-api.sh` へ追加:
1. `pull-script <path> <outfile>`
2. `push-script <path> <infile>`
3. `pull-flow <path> <outfile>`
4. `push-flow <json-file>`
5. `pull-all`scripts/flowsの一覧取得 + 一括保存)
6. `status-remote`ローカルとサーバーのhash比較
## Phase 2: メタ情報管理
- `state/remote_index.json` を導入し、`path -> hash/updated_at` を保持
- Pull前後で差分判定可能にする
## Phase 3: 標準化
- `docs/flow-manage` に操作例を固定
- 「作業開始時は必ずpull」を運用ルール化
## Phase 4: 半自動化(任意)
- AI/スクリプトで
- 変更検知
- Pull提案
- Push時の競合自動リカバリ
---
## 8. 標準運用手順Runbook
### 8.1 日常の更新(推奨)
1. `pull-all` でサーバー最新を取得
2. ローカル編集
3. `push-script` / `push-flow` で反映
4. 必要に応じてローカルGitにコミット
### 8.2 サーバーで変更されたものを取り込む
1. `status-remote` 実行
2. サーバー新規/更新分を `pull-*` で取得
3. ローカル履歴としてコミット(任意)
### 8.3 `alexa_speak` 更新例(現在の具体例)
1. `pull-script u/admin/alexa_speak scripts/alexa_speak.ts`
2. `scripts/alexa_speak.ts` を編集
3. `push-script u/admin/alexa_speak scripts/alexa_speak.ts`
4. Windmill UIで動作確認`device` がドロップダウン表示)
---
## 9. セキュリティ・監査方針
- APIトークンは環境変数またはローカル限定設定で管理
- リポジトリへ平文トークンをコミットしない
- 反映時刻・対象path・実行者をログ化
- サーバーGit Syncは監査証跡として維持
---
## 10. 障害前提の復旧設計(必須)
サーバー内部障害、API非冪等、通信断は防止不能とみなし、**復旧可能性を担保する**。
### 10.1 復旧の基本原則
1. Push前に対象オブジェクトをバックアップ保存必須
2. Push後に `post-verify`(再取得して期待値比較)を必須化
3. 失敗時は「再実行」ではなく「現状確認 -> 復旧」の順で実施
### 10.2 標準復旧手順
1. `get` で現状確認存在、content/value、version/hash
2. 期待状態との差分を判定
3. 必要ならバックアップから復元
4. 復元後に `post-verify`
5. 復旧ログを記録日時、path、操作者、原因、処置
### 10.3 フロー欠落時の緊急復旧
`delete -> create` の途中失敗でフローが消失した場合:
1. 直前バックアップJSONで即時 `flows/create`
2. 依存スケジュールの有効性を確認
3. 関連ジョブの手動実行で動作確認
---
## 11. Windmill依存を薄くする方針必須
Windmill API依存は避けられないため、依存点を最小化する。
### 11.1 使用APIの固定
- `scripts`: `list/get/create`
- `flows`: `list/get/create/delete`
- `schedules`: `list/get/create/delete`(必要時)
上記以外のAPIは原則使わない。
### 11.2 判定ロジックの優先順位
1. 実体JSON比較content/valueの比較を主判定
2. `hash` / `version_id` / `edited_at` は補助判定
3. 補助フィールド欠落時も動作継続できる実装にする
### 11.3 仕様変更検知
- 定期的に `smoke test` を実行してレスポンス形を検証
- 期待フィールド欠落時はPush停止fail closed
- 仕様差分を本ドキュメントの更新履歴に反映
---
## 12. 受け入れ条件
以下を満たせば「このプロジェクトはサーバーのワークフローを管理するためのもの」と言える状態:
1. ローカルから scripts/flows の Pull/Push がAPIで完結
2. サーバー更新がローカルで検知できるhash比較
3. 競合時の復旧手順が定義済み
4. 運用手順がこの文書だけで再現可能
---
## 13. 既知の注意点
1. `wm-api.sh` は現状 `bash` 前提だが、Windowsで改行がCRLFだと shebang 実行失敗する場合がある
2. フロー更新は環境により `PUT` できないため削除再作成を標準とする
3. API応答仕様はWindmillバージョン差で微差が出るため、初回導入時は `get` のレスポンス形を確認する
---
## 14. 更新履歴
| 日付 | 変更内容 |
|------|----------|
| 2026-03-03 | 初版作成API一本化方針、同期仕様、実装計画、Runbookを定義 |
| 2026-03-03 | 障害前提の復旧設計、Windmill依存を薄くする方針を必須要件として追記 |

View File

@@ -3,16 +3,55 @@
* 指定した Echo デバイスにテキストを読み上げさせる Windmill スクリプト
*
* パラメータ:
* device - デバイス名またはシリアル番号(例: "リビングエコー1", "プレハブ"
* device - ドロップダウンから選択するデバイス(内部的にはシリアル番号
* text - 読み上げるテキスト
*/
const ALEXA_API_URL = "http://alexa_api:3500";
type DeviceOption = { value: string; label: string };
const FALLBACK_DEVICE_OPTIONS: DeviceOption[] = [
{ value: "G0922H085165007R", label: "プレハブ (G0922H085165007R)" },
{ value: "G8M2DB08522600RL", label: "リビングエコー1 (G8M2DB08522600RL)" },
{ value: "G8M2DB08522503WF", label: "リビングエコー2 (G8M2DB08522503WF)" },
{ value: "G0922H08525302K5", label: "オフィスの右エコー (G0922H08525302K5)" },
{ value: "G0922H08525302J9", label: "オフィスの左エコー (G0922H08525302J9)" },
{ value: "G8M2HN08534302XH", label: "寝室のエコー (G8M2HN08534302XH)" },
];
// Windmill Dynamic Select: 引数名 `device` に対応する `DynSelect_device` と `device()` を定義
export type DynSelect_device = string;
export async function device(): Promise<DeviceOption[]> {
try {
const res = await fetch(`${ALEXA_API_URL}/devices`);
if (!res.ok) return FALLBACK_DEVICE_OPTIONS;
const devices = (await res.json()) as Array<{
name?: string;
serial?: string;
family?: string;
}>;
const options = devices
.filter((d) => d.family === "ECHO" && d.serial)
.map((d) => ({
value: d.serial as string,
label: `${d.name ?? d.serial} (${d.serial})`,
}))
.sort((a, b) => a.label.localeCompare(b.label, "ja"));
return options.length > 0 ? options : FALLBACK_DEVICE_OPTIONS;
} catch {
return FALLBACK_DEVICE_OPTIONS;
}
}
export async function main(
device: string,
device: DynSelect_device,
text: string,
): Promise<{ ok: boolean; device: string; text: string }> {
const ALEXA_API_URL = "http://alexa_api:3500";
const res = await fetch(`${ALEXA_API_URL}/speak`, {
method: "POST",
headers: { "Content-Type": "application/json" },