Split phrase list out into community-editable phrases.yaml
Some checks are pending
CI / test (push) Waiting to run

config.yaml previously mixed secrets with the phrase list, forcing
private-only edits. Now config supports phrases_file, which is loaded and
merged with any private phrases: entries, so the repo-tracked phrases.yaml
can be extended via PR while per-deployment additions stay private.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
awayatan 2026-07-26 23:27:27 +09:00
parent 6e023d1081
commit f411886d88
5 changed files with 115 additions and 25 deletions

View file

@ -27,7 +27,9 @@ Forgejo上にbot専用アカウント(例: `yaju-keisatsu-bot`)を作り、そ
cp config.example.yaml config.yaml
```
`config.yaml` を編集し、`forgejo.base_url` / `forgejo.token` / `webhook_secret` / `bot.username` を埋める。`phrases` は同梱の一部だけなので、必要に応じて自分の `config.yaml` に自由に追加してよい(このリストは本体の意図的にライトな内容にとどめてあります)。
`config.yaml` を編集し、`forgejo.base_url` / `forgejo.token` / `webhook_secret` / `bot.username` を埋める。
語録リストは [phrases.yaml](./phrases.yaml) にコミュニティ管理のものが同梱されており、`config.yaml``phrases_file: ./phrases.yaml` で読み込む(デフォルトで設定済み)。追加・削除の提案はPRで歓迎。自分の運用だけで使いたい追加語録があれば、それとは別に `config.yaml``phrases:` に書けば `phrases_file` の内容とマージされる。
`config.yaml` は秘密情報を含むため絶対にコミットしないこと(`.gitignore` 済み)。

View file

@ -1,4 +1,4 @@
# goroku-keisatsu の設定ファイル例。
# yaju-is-always-watching-you の設定ファイル例。
# コピーして config.yaml として使う。秘密情報が入るので config.yaml は絶対にコミットしないこと。
listen_addr: ":8080"
@ -11,13 +11,13 @@ forgejo:
# 自ホストのForgejoのベースURL
base_url: "https://git.example.dev"
# bot専用アカウントで発行したAPIトークン(Settings > Applications)。
# 権限は対象リポジトリへの read/write + issue + pull-request で十分。
# スコープは repository(write) + issue(write) で十分。
token: "change-me"
bot:
# 上のtokenを発行したbotアカウントのユーザー名。
# このアカウント自身の発言/pushには反応しない(無限ループ防止)。
username: "goroku-keisatsu-bot"
username: "yaju-keisatsu-bot"
# 是正PR用に作成するブランチの接頭辞。
branch_prefix: "bot/goroku-fix/"
# push時にスキャンする対象拡張子。空にすると全ファイルを対象にする。
@ -29,18 +29,10 @@ bot:
- ".js"
- ".ts"
# 検知する語録リスト。有名どころの一部だけを同梱しているので、
# 必要に応じて自分のconfig.yamlで自由に追加・削除すること。
phrases:
- "しょうもな"
- "たまげたなあ"
- "変態糞土方"
- "絶対に許さない"
- "24時間戦えますか"
- "本当に感謝しております"
- "巨大感謝"
- "覚悟はいいか?俺はできてる"
- "もうだめだ"
- "ンアッー"
- "ファッ!?"
- "24時間タタカエマスカ"
# phrases_file: リポジトリ同梱のコミュニティ管理語録リスト(phrases.yaml)を読む。
# 相対パスはこのconfig.yamlからの相対で解決される。
phrases_file: "./phrases.yaml"
# phrases: 自分の運用だけで使いたい追加語録があればここに書く(phrases_fileの内容とマージされる)。
# 空のままでもOK。
phrases: []

View file

@ -4,16 +4,25 @@ package config
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
type Config struct {
ListenAddr string `yaml:"listen_addr"`
WebhookSecret string `yaml:"webhook_secret"`
Forgejo Forgejo `yaml:"forgejo"`
Bot Bot `yaml:"bot"`
Phrases []string `yaml:"phrases"`
ListenAddr string `yaml:"listen_addr"`
WebhookSecret string `yaml:"webhook_secret"`
Forgejo Forgejo `yaml:"forgejo"`
Bot Bot `yaml:"bot"`
// PhrasesFile, if set, is loaded and merged with Phrases. It lets a
// deployment point at the repo's community-maintained phrases.yaml while
// still keeping any private additions inline below.
PhrasesFile string `yaml:"phrases_file"`
Phrases []string `yaml:"phrases"`
}
type phrasesFile struct {
Phrases []string `yaml:"phrases"`
}
type Forgejo struct {
@ -48,6 +57,22 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("parse config: %w", err)
}
if cfg.PhrasesFile != "" {
fp := cfg.PhrasesFile
if !filepath.IsAbs(fp) {
fp = filepath.Join(filepath.Dir(path), fp)
}
data, err := os.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("read phrases_file: %w", err)
}
var pf phrasesFile
if err := yaml.Unmarshal(data, &pf); err != nil {
return nil, fmt.Errorf("parse phrases_file: %w", err)
}
cfg.Phrases = append(pf.Phrases, cfg.Phrases...)
}
if cfg.Forgejo.BaseURL == "" {
return nil, fmt.Errorf("forgejo.base_url is required")
}
@ -58,7 +83,7 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("webhook_secret is required")
}
if len(cfg.Phrases) == 0 {
return nil, fmt.Errorf("phrases list is empty")
return nil, fmt.Errorf("phrases list is empty (set phrases and/or phrases_file)")
}
return cfg, nil

View file

@ -0,0 +1,55 @@
package config
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func TestLoad_PhrasesFileMergesWithInline(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "phrases.yaml"), "phrases:\n - しょうもな\n - たまげたなあ\n")
writeFile(t, filepath.Join(dir, "config.yaml"), `
listen_addr: ":8080"
webhook_secret: "s"
forgejo:
base_url: "http://forgejo:3000"
token: "t"
phrases_file: "./phrases.yaml"
phrases:
- 独自の追加語録
`)
cfg, err := Load(filepath.Join(dir, "config.yaml"))
if err != nil {
t.Fatalf("Load() error = %v", err)
}
want := []string{"しょうもな", "たまげたなあ", "独自の追加語録"}
if !reflect.DeepEqual(cfg.Phrases, want) {
t.Fatalf("Phrases = %v, want %v", cfg.Phrases, want)
}
}
func TestLoad_MissingPhrasesIsError(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "config.yaml"), `
webhook_secret: "s"
forgejo:
base_url: "http://forgejo:3000"
token: "t"
`)
if _, err := Load(filepath.Join(dir, "config.yaml")); err == nil {
t.Fatal("Load() error = nil, want error for empty phrases")
}
}

16
phrases.yaml Normal file
View file

@ -0,0 +1,16 @@
# コミュニティ管理の語録リスト(誰でもPRで追加・削除を提案してOK)。
# 個人の運用固有の追加語録は、この場所ではなく各自の config.yaml の
# `phrases:` に書くこと(config.yaml はコミットされないので分離される)。
phrases:
- "しょうもな"
- "たまげたなあ"
- "変態糞土方"
- "絶対に許さない"
- "24時間戦えますか"
- "本当に感謝しております"
- "巨大感謝"
- "覚悟はいいか?俺はできてる"
- "もうだめだ"
- "ンアッー"
- "ファッ!?"
- "24時間タタカエマスカ"