PR

Go言語(Golang)とAI駆動開発で実践するDDDベースのAPI開発|oapi-codegen(chi)・Bun・Codex・ハーネスエンジニアリング

3. 応用

こんにちは。Tomoyuki(@tomoyuki65)です。

私はこれまでGo言語(Golang)によるAPI開発方法について色々とご紹介してきましたが、2026年以降はAI駆動開発が主流になるのが避けられないため、これからはそれに合った方法が必要です。

そんな私もRailsやNext.jsでAI駆動開発を試して色々と学ぶことができたので、次はGo言語のAPI開発にも取り入れようと試してみました。

そこでこの記事では、私が試したGo言語(Golang)とAI駆動開発で実践するDDDベースのAPI開発方法についてご紹介します。

関連記事

Go言語(Golang)のEchoでシンプルかつ実務的なバックエンドAPI開発方法まとめ
こんにちは。Tomoyuki(@tomoyuki65)です。以前にGo言語のEchoでバックエンドAPIを開発する方法についての記事を書きましたが、あれから私自身もさらに成長し、もっとシンプルかつ実務的にAPIを開発する方法をまとめたいと思...
Go言語(Golang)のGinでDDD(ドメイン駆動設計)構成のバックエンドAPIを開発する方法まとめ
こんにちは。Tomoyuki(@tomoyuki65)です。これまではクリーンアーキテクチャを参考にしてGo言語のAPIの作り方を解説してきましたが、実務においてはDDD(ドメイン駆動設計)と呼ばれる方法で作られていることが多いです。そんな...
Go言語(Golang)のgRPCでDDD(ドメイン駆動設計)構成のバックエンドAPIを開発する方法まとめ
こんにちは。Tomoyuki(@tomoyuki65)です。直近にGo言語(Golang)のDDD(ドメイン駆動設計)に関する記事を書きましたが、gRPCにおいてもDDDで開発したいことがあると思います。そこでこの記事では、Go言語のgRP...
Go言語(Golang)でGraphQLのBFFを開発する方法まとめ
こんにちは。Tomoyuki(@tomoyuki65)です。マイクロサービスなどでバックエンドAPIが複数あり、エンドポイントが多くなりすぎてフロントエンド側(Webとアプリなど)で使いづらくなった場合、GraphQLのBFF(Backen...
DockerとGo言語(Golang)からPostgreSQLとORM「Bun」を使う方法
こんにちは。Tomoyuki(@tomoyuki65)です。Webサービスの開発では何らかのRDB(リレーショナルデータベース)を使うことが多いですが、これから新規開発をするなら将来の複雑化を見越してPostgreSQLを使うケースが増えて...

 

  1. Go言語(Golang)とAI駆動開発で実践するDDDベースのAPI開発|oapi-codegen(chi)・Bun・Codex・ハーネスエンジニアリング
    1. Go言語(Golang)のローカル開発環境構築
    2. golangci-lintの実行方法
    3. DDDベースのディレクトリ構成について
    4. 共通処理用のカスタムロガーを作成する
    5. oapi-codegenでHealthcheck(ヘルスチェック)APIを作る
    6. Dockerコンテナの再ビルドと起動
    7. ユニットテストの実行
    8. インテグレーションテストの作成と実行
    9. Healthcheck(ヘルスチェック)APIをPostmanで試す
  2. OpenAI「Codex」におけるハーネスエンジニアリング設定
    1. ハーネスエンジニアリングとは?
    2. OpenAI「Codex」におけるハーネス設計について
    3. codexの設定ファイルを作成
    4. sandbox外のコマンド制御設定を追加
    5. agents(サブエージェント)機能を追加
    6. workflows(ワークフロー)機能を追加
    7. Agent Skillsを追加
      1. スキル「plan-to-issue」:プランモードで作成した開発計画をGitHubのIssue用のフォーマットへ変換して自動登録する
      2. スキル「auto-commit」:修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する
      3. スキル「tdd-draft-pr」:Issueと追加したREDテストをもとに、テストレビュー用のドラフトPRを作成する
      4. スキル「pr-sync-comment」:直前のコミット内容をもとにPRコメントを生成し、ブランチをpushした上でPRにコメントを追加する
      5. スキル「tdd-ready-pr」:レビュー済みのドラフトPRに実装レビューコメントを追加し、PRをReady for Reviewへ変更する
    8. 今回のプロジェクト用のハーネス設定
    9. ハーネス設計の注意点
  3. Lefthook・Git・GitHubを利用して管理する
    1. Gitフック管理ツール「Lefthook」を導入(任意)
    2. GitHub ActionsによるCIの導入
    3. Git管理
    4. GitHubのリポジトリに登録
    5. mainブランチなどの保護設定をしたい場合
    6. ラベル追加(任意)
  4. OpenAI「Codexアプリ」のプランモードで開発計画を立てる
  5. OpenAI「Codexアプリ」でタスク実行する
  6. AI駆動開発で作ったAPIの検証
    1. AI駆動開発における開発効率について
  7. レビュー完了後にブランチのマージとIssueのクローズ
  8. 本番環境用のDockerコンテナについて
  9. AIツール利用時のセキュリティ対策について
  10. 最後に

Go言語(Golang)とAI駆動開発で実践するDDDベースのAPI開発|oapi-codegen(chi)・Bun・Codex・ハーネスエンジニアリング

この記事ではmacOS(Apple silicon)のPCを使い、パッケージ管理にHomebrew、コード管理に「Git」やGitHubローカル開発環境に「Docker」、AIツールはOpenAIのCodexを利用する前提で進めます。

以降の内容を参考にしたい場合は、事前にこれらを利用できるように準備して下さい。

※GitHub CLIとして「gh」コマンドも準備が必要です。これはHomebrewでインストールできます。

 

関連記事

OpenAI Codex CLI / Codexアプリの使い方【ChatGPT時代のAI開発ツール入門】
こんにちは。Tomoyuki(@tomoyuki65)です。2026年現在、AI戦国時代に突入し、めちゃめちゃ競争が激しいところですが、ついにOpenAI も新機能としてCodexアプリをリリースしたり、各種キャンペーンを展開したりしていま...

 

Go言語(Golang)のローカル開発環境構築

まずはGo言語(Golang)のローカル開発環境構築を作るため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir go-oapi-aidd && cd go-oapi-aidd
$ mkdir -p deploy/docker/local/db && touch deploy/docker/local/db/Dockerfile
$ mkdir -p deploy/docker/local/db/init && touch deploy/docker/local/db/init/init.sql
$ mkdir -p deploy/docker/local/go && touch deploy/docker/local/go/Dockerfile
$ mkdir -p src/internal/infrastructure/database && touch src/internal/infrastructure/database/bun.go
$ mkdir -p src/internal/infrastructure/database/schema && touch src/internal/infrastructure/database/schema/.keep
$ mkdir -p src/cmd/migrate && touch src/cmd/migrate/main.go
$ mkdir -p touch src/internal/infrastructure/migrations && touch src/internal/infrastructure/migrations/migrations.go src/internal/infrastructure/migrations/.keep.sql
$ mkdir -p src/internal/infrastructure/observability && touch src/internal/infrastructure/observability/tracer.go
$ touch src/main.go src/.golangci.yaml .env compose.yaml .gitignore

 

・「deploy/docker/local/db/Dockerfile」

FROM postgres:18.3

ENV LANG ja_JP.utf8

# PostgreSQLの日本語化で「ja_JP.utf8」を使うために必要
RUN apt-get update && \
    apt-get install -y locales && \
    rm -rf /var/lib/apt/lists/* && \
    localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8

 

・「deploy/docker/local/db/init/init.sql」

-- テスト用DB作成
CREATE DATABASE "testing-db";

 

・「deploy/docker/local/go/Dockerfile」

FROM golang:1.26.3-alpine3.23

# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(--no-cacheでキャッシュ削除)
    apk add --no-cache \
            curl \
            postgresql-client

WORKDIR /go/src

COPY ./src .

# go.modがあれば依存関係をインストール
RUN if [ -f ./go.mod ]; then \
      go install; \
    fi

# 開発用のライブラリをインストール
RUN go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.0
RUN go install github.com/air-verse/air@v1.65.3
RUN curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.12.2
RUN go install go.uber.org/mock/mockgen@v0.6.0

EXPOSE 8080

※今回はGo言語のバージョン「1.26.3」、OpenAPI(旧Swagger)の定義からGoの入出力部分のコードを生成するツール「oapi-codegen」、formatterやlinterに「golangci-lint」、モック用ライブラリに「mockgen」を使います。

 

・「src/internal/infrastructure/migrations/migrations.go」

package migrations

import (
    "embed"

    "github.com/uptrace/bun/migrate"
)

//go:embed *.sql
var sqlMigrations embed.FS

var Migrations = migrate.NewMigrations()

func init() {
    if err := Migrations.Discover(sqlMigrations); err != nil {
        panic(err)
    }
}

※これはORM「Bun」でマイグレーション用の設定ファイルです。

 

・「src/cmd/migrate/main.go」

package main

import (
    "fmt"
    "log/slog"
    "os"
    "strings"

    "github.com/uptrace/bun/migrate"
    "github.com/urfave/cli/v2"

    "go-oapi-aidd/internal/infrastructure/database"
    "go-oapi-aidd/internal/infrastructure/migrations"
)

func main() {
    // DBインスタンスの取得
    db, err := database.NewBunDB()
    if err != nil {
        slog.Error("failed to connect database", "err", err)
        os.Exit(1)
    }
    defer db.Close()

    // マイグレーション用ツール設定
    migrator := migrate.NewMigrator(db, migrations.Migrations)

    app := &cli.App{
        Name: "migrate",
        Usage: "database migrations tool",
        Commands: []*cli.Command{
            // 初期化用(マイグレーション管理用テーブルの作成。最初に一度だけ実行する。)
            {
                Name: "init",
                Usage: "create migration tables",
                Action: func(c *cli.Context) error { return migrator.Init(c.Context) },
            },
            // SQLファイル作成用(upとdown用の二つを作成する。)
            {
                Name: "create_sql",
                Usage: "create up and down SQL migrations",
                Action: func(c *cli.Context) error {
                    name := strings.Join(c.Args().Slice(), "_")
                    files, err := migrator.CreateSQLMigrations(c.Context, name)
                    if err != nil {
                        return err
                    }
                    for _, f := range files {
                        fmt.Printf("created %s (%s)\n", f.Name, f.Path)
                    }
                    return nil
                },
            },
            // マイグレーション状態の確認
            {
                Name: "status",
                Usage: "print migrations status",
                Action: func(c *cli.Context) error {
                    ms, err := migrator.MigrationsWithStatus(c.Context)
                    if err != nil {
                        return err
                    }
                    fmt.Printf("migrations: %s\n", ms)
                    fmt.Printf("unapplied migrations: %s\n", ms.Unapplied())
                    fmt.Printf("last migration group: %s\n", ms.LastGroup())
                    return nil
                },
            },
            // マイグレーションの実行
            // (同一タイミングで実行したSQLファイルを一つのグループとして管理している。)
            {
                Name: "migrate",
                Usage: "migrate database",
                Action: func(c *cli.Context) error {
                    group, err := migrator.Migrate(c.Context)
                    if err != nil {
                        return err
                    }
                    if group.IsZero() {
                        fmt.Println("no new migrations")
                        return nil
                    }
                    fmt.Printf("migrated to %s\n", group)
                    return nil
                },
            },
            // ロールバックの実行
            // (グループ単位で管理していて、それを一つ前に戻す。)
            {
                Name: "rollback",
                Usage: "rollback the last migration group",
                Action: func(c *cli.Context) error {
                    group, err := migrator.Rollback(c.Context)
                    if err != nil {
                        return err
                    }
                    if group.IsZero() {
                        fmt.Println("no groups to rollback")
                        return nil
                    }
                    fmt.Printf("rolled back %s\n", group)
                    return nil
                },
            },
        },
    }

    if err := app.Run(os.Args); err != nil {
        slog.Error("failed to run migration", "err", err)
        os.Exit(1)
    }
}

※これはマイグレーション実行用のスクリプトファイルです。

 

・「src/internal/infrastructure/database/bun.go」

package database

import (
    "database/sql"
    "fmt"
    "os"
    "strconv"
    "time"

    "github.com/uptrace/bun"
    "github.com/uptrace/bun/dialect/pgdialect"
    "github.com/uptrace/bun/driver/pgdriver"
    "github.com/uptrace/bun/extra/bundebug"
    "github.com/uptrace/bun/extra/bunotel"
)

// NewDB はBunのDB接続インスタンスを生成します
func NewBunDB() (*bun.DB, error) {
    // 環境変数「ENV」の値を取得
    env := os.Getenv("ENV")
    if env == "" {
        env = "local"
    }

    // DB接続用の環境変数の設定
    var dbHost, dbPort, dbName, dbUser, dbPassword, dbMaxOpenCons, dbMaxIdleCons, dbConnMaxLifetime string

    if dbHost = os.Getenv("DB_HOST"); dbHost == "" {
        dbHost = "localhost"
    }

    if dbPort = os.Getenv("DB_PORT"); dbPort == "" {
        dbPort = "5432"
    }

    if dbName = os.Getenv("DB_NAME"); dbName == "" {
        dbName = "local-db"
    }

    if env == "testing" {
        dbName = "testing-db"
    }

    if dbUser = os.Getenv("DB_USER"); dbUser == "" {
        dbUser = "local-db-user"
    }

    if dbPassword = os.Getenv("DB_PASSWORD"); dbPassword == "" {
        dbPassword = "local-db-password"
    }

    if dbMaxOpenCons = os.Getenv("DB_MAX_OPEN_CONNS"); dbMaxOpenCons == "" {
        dbMaxOpenCons = "20"
    }

    if dbMaxIdleCons = os.Getenv("DB_MAX_IDLE_CONNS"); dbMaxIdleCons == "" {
        dbMaxIdleCons = "10"
    }

    if dbConnMaxLifetime = os.Getenv("DB_CONN_MAX_LIFETIME"); dbConnMaxLifetime == "" {
        dbConnMaxLifetime = "5"
    }

    // DSN設定
    var dsn string
    if env == "production" {
        dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s", dbUser, dbPassword, dbHost, dbPort, dbName)
    } else {
        dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbUser, dbPassword, dbHost, dbPort, dbName)
    }

    // sql.DB の初期化(pgdriverを使用)
    sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))

    if err := sqldb.Ping(); err != nil {
        return nil, err
    }

    // コネクションプールの設定
    maxOpenCons, _ := strconv.Atoi(dbMaxOpenCons) // 最大接続数
    sqldb.SetMaxOpenConns(maxOpenCons)

    maxIdleCons, _ := strconv.Atoi(dbMaxIdleCons) // アイドル時の保持接続数
    sqldb.SetMaxIdleConns(maxIdleCons)

    maxLifetime, _ := strconv.Atoi(dbConnMaxLifetime) // 接続の寿命(分)
    sqldb.SetConnMaxLifetime(time.Duration(maxLifetime) * time.Minute)

    // BunDBのインスタンス生成
    db := bun.NewDB(sqldb, pgdialect.New())

    // デバッグログ設定(SQL表示)
    if env != "production" {
        db.AddQueryHook(bundebug.NewQueryHook(
            bundebug.WithEnabled(true), // デバッグログ機能の有効化
            bundebug.WithVerbose(true), // ログ詳細表示を有効化
        ))
    }

    // OpenTelemetryの設定
    db.AddQueryHook(bunotel.NewQueryHook(
        bunotel.WithDBName("BunDB"),
    ))

    return db, nil
}

※これはORM「Bun」の接続設定です。

 

・「src/internal/infrastructure/observability/tracer.go」

package observability

import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
    "go.opentelemetry.io/otel/sdk/resource"
    sdkTrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)

func InitTracer(ctx context.Context, exporterType string) func(context.Context) error {
    switch exporterType {
    case "local":
        // 標準出力向けの Trace Exporter を作成
        exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
        if err != nil {
            panic(err)
        }

        // TracerProvider を構築し、サービス情報と Exporter を設定
        tp := sdkTrace.NewTracerProvider(
            sdkTrace.WithBatcher(exporter),
            sdkTrace.WithResource(resource.NewWithAttributes(
                semconv.SchemaURL,
                semconv.ServiceName("go-oapi-aidd"),
            )),
        )

        // アプリケーション全体で利用する TracerProvider として登録
        otel.SetTracerProvider(tp)

        return tp.Shutdown

    default:
        // 何もしない関数を返す
        return func(context.Context) error {
            return nil
        }
    }
}

※これはOpenTelemetry用のトレース出力先の初期設定です。

 

・「src/main.go」

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World !!")
}

 

・「src/.golangci.yaml」

version: "2"

run:
  # タイムアウト設定
  timeout: 5m

formatters:
  # 実行するformatterを指定
  enable:
    - gofumpt
    - gci

  settings:
    gofumpt:
      # モジュールパス設定
      module-path: go-oapi-aidd
    gci:
      # 並び順設定
      custom-order: true
      sections:
        - standard
        - default
        - prefix(go-oapi-aidd)

linters:
  # デフォルト設定を全てOFF
  default: none

  # 実行するlintを指定
  enable:
    - govet
    - staticcheck
    - gosec

※これはformatterやlinter用のライブラリ「golangci-lint」の設定ファイルです。

 

・「.env」

ENV=local
PORT=8080
REQUEST_TIMEOUT_SECONDS=10
CORS_ALLOWED_ORIGIN=*
OTEL_EXPORTER_TYPE=none
DB_NAME=local-db
DB_USER=root
DB_PASSWORD=root-pass
DB_HOST=db
DB_PORT=5432
DB_MAX_OPEN_CONNS=2
DB_MAX_IDLE_CONNS=2
DB_CONN_MAX_LIFETIME=5

※今回はこのような環境変数を使い、それを.envを使って利用しますが、このような機密情報を扱う可能性があるものについては、ちゃんと使い方を理解してから使うようにして下さい。特にこれからのAI駆動開発では、AIツールから見える範囲にAPIキーなどの機密情報を置くべきではない(.envだろうが、1Password CLIだろうが、根本原因の解決にならず、プロンプトインジェクションの可能性を防げない)ので、APIキーを使う必要がある場合などは、そもそもAIツールを使わずに開発するなどして下さい。

 

関連記事

生成AIで機密情報は大丈夫?AIツール開発のセキュリティ対策|ゼロトラスト・サンドボックス・環境変数(.env)管理
こんにちは。Tomoyuki(@tomoyuki65)です。近年は生成AIが急激に普及し、非エンジニアの方も含めて、様々な方が各種AIツールを利用し始めていると思います。ただその一方で、"機密情報の漏洩"といったセキュリティ問題も多発してお...

 

・「compose.yaml」

services:
  api:
    container_name: go-oapi-aidd-api
    build:
      context: .
      dockerfile: ./deploy/docker/local/go/Dockerfile
    command: air -c .air.toml
    volumes:
      - ./src:/go/src
    ports:
      - "8080:8080"
    env_file:
      - .env
    tty: true
    stdin_open: true
    depends_on:
      db:
        condition: service_healthy
  db:
    container_name: go-oapi-aidd-db
    build:
      context: .
      dockerfile: ./deploy/docker/local/db/Dockerfile
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      TZ: Asia/Tokyo
      # ローカル環境でもパスワードを有効化する設定
      POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256
    volumes:
      - go-oapi-aidd-db-data:/var/lib/postgresql
      # 初回起動時にSQLを実行する設定
      - ./deploy/docker/local/db/init:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"
    env_file:
      - .env
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 5
volumes:
  go-oapi-aidd-db-data:

 

・「.gitignore」

.DS_Store
.env
/src/tmp

※これはgitで管理対象外にするファイル設定です。

 

次に以下のコマンドを実行し、Dockerコンテナをビルドします。

$ docker compose build --no-cache

 

次に以下のコマンドを実行し、初期化処理をします。

$ docker compose run --rm api go mod init go-oapi-aidd
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api air init

 

次に以下のコマンドを実行し、スクリプトの実行を試します。

$ docker compose run --rm api go run main.go

 

コマンド実行後、以下のように「Hello World !!」が出力されればOKです。

 

golangci-lintの実行方法

今回はformatterやlinterにライブラリ「golangci-lint」を使います。実行したい場合は、以下のコマンドを利用して下さい。

・formatterの実行

$ docker compose run --rm api golangci-lint fmt -v ./...

 

・linterの実行

$ docker compose run --rm api golangci-lint run -v ./...

 

関連記事

Go言語(Golang)golangci-lintの使い方|おすすめformatter・lintの設定方法
こんにちは。Tomoyuki(@tomoyuki65)です。Go言語(Golang)には標準のformatter(go fmt)やlint(go vet)が使えるようになっていますが、実務では他にも定番のライブラリがよく使われたりします。そ...

 

DDDベースのディレクトリ構成について

今回はDDD(ドメイン駆動設計)ベースのディレクトリ構成でAPIを作りますが、全面的にDDDを適用するのは本質的ではないケースも多く、実装や運用コストも高くなりがちです。

そこで今回はDDDの本質を踏まえて、まず対象のAPIが以下の3つの業務領域に分類することを前提としています。

・中核の業務領域
・補完的な業務領域
・一般的な業務領域

 

それを踏まえて、ビジネス上の競争優位性に直結する「中核の業務領域」に対してのみDDDを適用し、「補完的な業務領域」ならトランザクションスクリプト、「一般的な業務領域」ならアクティブレコードを適用するような構成としています。

以上を踏まえて、今回のディレクトリ構成は以下のように設計します。

/go-oapi-adii
 └── /src
      └── /internal
           ├── /core(中核の業務領域)
           |    |
           |    └── /[domain_name](ドメインモジュール)
           |         |
           |         ├── /domain(ドメイン)
           |         |   |
           |         |   ├── [entity_name].go(エンティティ)
           |         |   |
           |         |   ├── [valueobject_name].go(値オブジェクト)
           |         |   |
           |         |   ├── [domain_service_name]_service.go(ドメインサービス)
           |         |   |
           |         |   ├── [repository_name]_repository.go(リポジトリのインターフェース)
           |         |   |
           |         |   └── [gateway_name]_gateway.go(外部サービス用のインターフェース)
           |         |
           |         ├── /usecase(ユースケース層)
           |         |
           |         └── /infrastructure(ドメイン用のインフラストラクチャ層)
           |              |
           |              ├── /repository(リポジトリの実装)
           |              |    |
           |              |    ├── /command(書き込み)
           |              |    |
           |              |    └── /query(読み込み)
           |              |
           |              └── /external(外部サービスの実装)
           |
           ├── /supporting(補完的なの業務領域)
           |    |
       |    └── /[supporting_name](サービス層)
           |         |
           |         └── service.go
           |
           ├── /generic(一般的な業務領域)
           |
           ├── /shared(横断関心)
           |
           └── /di(DIコンテナ層)
               |
               └── container.go

※DDDのレイヤードアーキテクチャをベースにドメイン単位で「domain」、「usecase」、「infrastructure」を分離したモジュラーモノリス構成を採用しています。

 

関連記事

ドメイン駆動設計(Domain-Driven Design / DDD)の本質と大事なこと
こんにちは。Tomoyuki(@tomoyuki65)です。Go言語(Golang)の仕事では、よくドメイン駆動設計(Domain-Driven Design / DDD)での開発経験が求められたりします。これはマイクロサービスとして開発す...

 

共通処理用のカスタムロガーを作成する

次に共通処理用としてカスタムロガーを作成するため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir -p src/internal/shared/logger && touch src/internal/shared/logger/logger.go
$ mkdir -p src/internal/infrastructure/logger && touch src/internal/infrastructure/logger/logger_slog.go

 

次に作成したファイルをそれぞれ以下のように記述します。

・「src/internal/shared/logger/logger.go」

package logger

import (
    "context"
)

type Logger interface {
    Info(addSource bool, tx context.Context, message string)
    Warn(addSource bool, tx context.Context, message string)
    Error(addSource bool, tx context.Context, message string)
}

 

・「src/internal/infrastructure/logger/logger_slog.go」

package logger

import (
    "context"
    "log/slog"
    "os"
    "runtime"

    "github.com/go-chi/chi/v5/middleware"

    "go-oapi-aidd/internal/shared/logger"
)

// slogの設定
type SlogHandler struct {
    slog.Handler
}

func (h *SlogHandler) Handle(ctx context.Context, r slog.Record) error {
    // rをコピー
    newRecord := r.Clone()

    // runtimeからPCを取得して上書き
    pc, _, _, ok := runtime.Caller(4)
    if ok {
        newRecord.PC = pc
    }

    // ミドルウェアで設定したリクエストIDを取得
    requestID := middleware.GetReqID(ctx)

    if requestID != "" {
        newRecord.AddAttrs(
            slog.String("request_id", requestID),
        )
    }

    return h.Handler.Handle(ctx, newRecord)
}

var slogHandler = &SlogHandler{
    slog.NewJSONHandler(os.Stdout, nil),
}

var slogHandlerAddSource = &SlogHandler{
    slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        AddSource: true,
    }),
}

var (
    newLogger = slog.New(slogHandler)
    newLoggerAddSource = slog.New(slogHandlerAddSource)
)

// ロガーの設定
type slogLogger struct{}

func NewSlogLogger() logger.Logger {
    return &slogLogger{}
}

func (l *slogLogger) Info(addSource bool, ctx context.Context, message string) {
    if env := os.Getenv("ENV"); env != "testing" {
        if addSource {
            newLoggerAddSource.InfoContext(ctx, message)
        } else {
            newLogger.InfoContext(ctx, message)
        }
    }
}

func (l *slogLogger) Warn(addSource bool, ctx context.Context, message string) {
    if env := os.Getenv("ENV"); env != "testing" {
        if addSource {
            newLoggerAddSource.WarnContext(ctx, message)
        } else {
            newLogger.WarnContext(ctx, message)
        }
    }
}

func (l *slogLogger) Error(addSource bool, ctx context.Context, message string) {
    if env := os.Getenv("ENV"); env != "testing" {
        if addSource {
            newLoggerAddSource.ErrorContext(ctx, message)
        } else {
            newLogger.ErrorContext(ctx, message)
        }
    }
}

 

次に以下のコマンドを実行し、モック化用のファイルも作成しておきます。

$ docker compose run --rm api mockgen -source=./internal/shared/logger/logger.go -destination=./internal/shared/logger/mock_logger/mock_logger.go

 

コマンド実行後、ファイル「src/internal/shared/logger/mock_logger/mock_logger.go」が作成されればOKです。

※もしファイル「src/internal/shared/logger/logger.go」のインターフェース定義を修正した場合は、再度コマンドを実行してモック化用のファイルも更新する必要があります。

 

oapi-codegenでHealthcheck(ヘルスチェック)APIを作る

今回は「oapi-codegen」を使ってOpenAPIの定義から入出力部分のコードを生成し、そこから内部処理のコードを作ってAPIを作ります。

※AIの力を借りることでOpenAPIの定義を書く難易度が下がったため、AI駆動開発前提であれば、OpenAPI定義から一部のコードを生成してAPIを作った方が効率がいいです。そのため、今回はルーター部分が選択可能でバランスが良さそうな「oapi-codegen」を利用しています。また、AI駆動開発前提なら可能な限りGo言語の標準パッケージのみで作った方がいいですが、そうするとミドルウェアなども個別に書く必要がでて面倒だったりするため、今回はGoの標準パッケージを中心に作ることを前提とした設計思想のフレームワーク「chi」を利用しています。(逆にいうと、今まで人気だったGinやechoは今後使われなくなりそうかも。。)

 

まずは簡単な例として、Healthcheck(ヘルスチェック)APIを作るため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir -p src/openapi/components/schemas/common && touch src/openapi/components/schemas/common/message.yaml
$ mkdir -p src/openapi/paths/supporting && touch src/openapi/paths/supporting/healthcheck.yaml
$ touch src/openapi/openapi.yaml src/oapi-codegen.server.yaml src/oapi-codegen.models.yaml Makefile

 

次に作成したファイルをそれぞれ以下のように記述します。

・「src/openapi/components/schemas/common/message.yaml」

type: object
required:
  - message
properties:
  message:
    type: string

※メッセージ用のスキーマ定義

 

・「src/openapi/paths/supporting/healthcheck.yaml」

get:
  operationId: healthcheck
  tags:
    - Healthcheck
  description: サービスの稼働状態を確認します。
  responses:
    "200":
      description: ヘルスチェック成功
      content:
        application/json:
          schema:
            $ref: "../../openapi.yaml#/components/schemas/HealthcheckResponse"
          example:
            message: OK
    "500":
      description: 予期せぬエラーが発生
      content:
        application/json:
          schema:
            $ref: "../../openapi.yaml#/components/schemas/ErrorResponse"
          example:
            message: Internal Server Error

※Healthcheck(ヘルスチェック)APIの定義

 

・「src/openapi/openapi.yaml」

openapi: 3.0.3
info:
  title: go-oapi-aidd API
  version: 1.0.0

servers:
  - url: http://localhost:8080/api/v1
    description: ローカル v1

tags:
  - name: Healthcheck
    description: ヘルスチェックAPI

paths:
  /healthcheck:
    $ref: "./paths/supporting/healthcheck.yaml"

components:
  schemas:
    HealthcheckResponse:
      $ref: "./components/schemas/common/message.yaml"
    ErrorResponse:
      $ref: "./components/schemas/common/message.yaml"

※OpenAPI全体の定義。入出力の定義はcomponents部分で定義し、それをpathsの各種API用の定義で利用する。

 

・「src/oapi-codegen.server.yaml」

package: gen
generate:
  chi-server: true
  strict-server: true
  embedded-spec: true
output: internal/presentation/gen/server.gen.go
import-mapping:
  ../../openapi.yaml: "-"

※oapi-codegenでserver用のコード生成のための定義

 

・「src/oapi-codegen.models.yaml」

package: gen
generate:
  models: true
output: internal/presentation/gen/models.gen.go
import-mapping:
  ../../openapi.yaml: "-"
output-options:
  skip-prune: true

※oapi-codegenで入出力用のmodelsのコード生成のための定義

 

・「Makefile」

.PHONY: generate \
    test test-unit test-integration test-e2e

generate:
    docker compose run --rm api oapi-codegen -config oapi-codegen.models.yaml openapi/openapi.yaml
    docker compose run --rm api oapi-codegen -config oapi-codegen.server.yaml openapi/openapi.yaml

test: test-unit test-integration test-e2e

test-unit:
    docker compose exec api env ENV=testing go test -v -cover -tags=unit $$(docker compose exec api env ENV=testing go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=unit ./...)

test-integration:
    docker compose exec api env ENV=testing go test -v -tags=integration $$(docker compose exec api env ENV=testing go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=integration ./...)

test-e2e:
    docker compose exec api env ENV=testing go test -v -tags=e2e $$(docker compose exec api env ENV=testing go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=e2e ./...)

※OpenAPIの定義からコード生成するためのコマンドおよび、テスト実行時に利用するコマンドをMakefileで定義して実行しやすくしています。

 

次に以下のコマンドを実行し、OpenAPIの定義からコードを生成します。

$ make generate

 

コマンド実行後、ファイル「src/internal/presentation/gen/server.gen.go」、「src/internal/presentation/gen/models.gen.go」が作成されればOKです。

※コマンドを実行するとファイルは更新されます。OpenAPIの定義を修正後にコマンドを実行して更新して下さい。尚、入出力の構造体の定義は「models.gen.go」に作成されます。

 

次にAPIの中身を作るため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir -p src/internal/supporting/healthcheck
$ touch src/internal/supporting/healthcheck/service.go src/internal/supporting/healthcheck/service_test.go

※ハンドラーから呼び出すユースケース用のファイルを作り、APIの中身を記述して作る。

 

次に作成されたファイルをそれぞれ以下のように記述します。

・「src/internal/supporting/healthcheck/service.go」

package healthcheck

import (
    "context"
    "fmt"

    "github.com/uptrace/bun"

    sl "go-oapi-aidd/internal/shared/logger"
)

type Service interface {
    Execute(ctx context.Context) error
}

type service struct {
    db *bun.DB
    logger sl.Logger
}

func NewService(db *bun.DB, logger sl.Logger) Service {
    return &service{
        db: db,
        logger: logger,
    }
}

func (s *service) Execute(ctx context.Context) error {
    // ログ出力
    s.logger.Info(true, ctx, "Healthcheck処理を実行(DB接続チェック含む)")

    // DB接続チェック
    if err := s.db.Ping(); err != nil {
        msg := fmt.Sprintf("failed to ping database: %s", err.Error())
        s.logger.Error(true, ctx, msg)
        return err
    }

    return nil
}

 

・「src/internal/supporting/healthcheck/service_test.go」

//go:build unit

package healthcheck

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "go-oapi-aidd/internal/infrastructure/database"
    mockLogger "go-oapi-aidd/internal/shared/logger/mock_logger"
)

func TestService_Execute(t *testing.T) {
    // DB取得
    db, err := database.NewBunDB()
    if err != nil {
        t.Fatal("failed to connect database")
    }

    // ロガーのモック化
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    mockLogger := mockLogger.NewMockLogger(ctrl)

    t.Run("正常終了すること", func(t *testing.T) {
        // モック設定
        mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)

        // サービス定義
        service := NewService(db, mockLogger)

        // テスト実行
        err := service.Execute(context.Background())

        // 検証
        assert.NoError(t, err)
    })

    t.Run("異常終了すること", func(t *testing.T) {
        // モック設定
        mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
        mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)

        // サービス定義
        db.Close()
        service := NewService(db, mockLogger)

        // テスト実行
        err := service.Execute(context.Background())

        // 検証
        assert.Error(t, err)
    })
}

 

次に以下のコマンドを実行し、モック化用のファイルも作成しておきます。

$ docker compose run --rm api mockgen -source=./internal/supporting/healthcheck/service.go -destination=./internal/supporting/healthcheck/mock_service/mock_service.go

 

コマンド実行後、ファイル「src/internal/shared/logger/mock_logger/mock_logger.go」が作成されればOKです。

※もしファイル「src/internal/supporting/healthcheck/mock_service/mock_service.go」のインターフェース定義を修正した場合は、再度コマンドを実行してモック化用のファイルも更新する必要があります。

 

次にDIコンテナ用のファイルを作成するため、以下のコマンドを実行します。

$ mkdir -p src/internal/di && touch src/internal/di/container.go

 

次に作成したファイルを以下のように記述します。

・「src/internal/di/container.go」

package di

import (
    "github.com/uptrace/bun"

    sl "go-oapi-aidd/internal/shared/logger"
    "go-oapi-aidd/internal/supporting/healthcheck"
)

// DIコンテナの定義
type Container struct {
    Logger sl.Logger
    HealthcheckService healthcheck.Service
}

// 依存関係の定義
type Dependencies struct{}

// 依存関係の上書き用関数のコンテナオプション定義
type ContainerOption func(*Dependencies)

// デフォルトの依存関係の作成関数
func NewDefaultDependencies() Dependencies {
    return Dependencies{}
}

// 依存関係からDIコンテナの作成関数
func NewContainerFromDependencies(db *bun.DB, logger sl.Logger, deps Dependencies) *Container {
    return &Container{
        Logger: logger,
        HealthcheckService: healthcheck.NewService(db, logger),
    }
}

// DIコンテナの作成関数
func NewContainer(db *bun.DB, logger sl.Logger, opts ...ContainerOption) *Container {
    // デフォルトの依存関係の取得
    deps := NewDefaultDependencies()

    // 依存関係の上書き処理
    for _, opt := range opts {
        opt(&deps)
    }

    return NewContainerFromDependencies(db, logger, deps)
}

※もしリポジトリなどの依存関係がある場合は「type Dependencies struct{}」と「func NewDefaultDependencies() Dependencies {}」に定義を追加、そして上書き用のオプション関数を別途作成し、テストコードでモック化できるようにして下さい。

// Dependenciesに依存関係の定義を追加
type Dependencies struct {
    UserRepository repository.UserRepository
}

// NewDefaultDependenciesにも依存関係の定義を追加
func NewDefaultDependencies() Dependencies {
    return Dependencies{
        UserRepository: repository.NewUserRepository(),
    }
}

// モック化用に上書き用のオプション関数を作成
func WithUserRepository(repo repository.UserRepository) ContainerOption {
    return func(deps *Dependencies) {
        deps.UserRepository = repo
    }
}

 

次にハンドラー用のファイルを作成するため、以下のコマンドを実行します。

$ mkdir -p src/internal/presentation/handler/supporting/healthcheck
$ touch src/internal/presentation/handler/supporting/healthcheck/healthcheck_handler.go
$ touch src/internal/presentation/handler/supporting/healthcheck/healthcheck_handler_test.go
$ touch src/internal/presentation/handler/handler_v1.go

 

次に作成したファイルをそれぞれ以下のように記述します。

・「src/internal/presentation/handler/supporting/healthcheck/healthcheck_handler.go」

package healthcheck

import (
    "context"

    "go-oapi-aidd/internal/di"
    "go-oapi-aidd/internal/presentation/gen"
    "go-oapi-aidd/internal/supporting/healthcheck"
)

type HealthcheckHandler struct {
    container *di.Container
    healthcheckService healthcheck.Service
}

func NewHealthcheckHandler(container *di.Container, healthcheckService healthcheck.Service) *HealthcheckHandler {
    return &HealthcheckHandler{
        container: container,
        healthcheckService: healthcheckService,
    }
}

func (h *HealthcheckHandler) Healthcheck(
    ctx context.Context,
    request gen.HealthcheckRequestObject,
) (gen.HealthcheckResponseObject, error) {
    if err := h.healthcheckService.Execute(ctx); err != nil {
        return gen.Healthcheck500JSONResponse{
            Message: "Internal Server Error",
        }, nil
    } else {
        return gen.Healthcheck200JSONResponse{
            Message: "OK",
        }, nil
    }
}

 

・「src/internal/presentation/handler/supporting/healthcheck/healthcheck_handler_test.go」

//go:build unit

package healthcheck

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "go-oapi-aidd/internal/di"
    "go-oapi-aidd/internal/infrastructure/database"
    "go-oapi-aidd/internal/presentation/gen"
    mockLogger "go-oapi-aidd/internal/shared/logger/mock_logger"
    mockService "go-oapi-aidd/internal/supporting/healthcheck/mock_service"
)

func TestHealthcheckHandler_Healthcheck(t *testing.T) {
    // DB取得
    db, err := database.NewBunDB()
    if err != nil {
        t.Fatal("failed to connect database")
    }

    // ロガーのモック化
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    mockLogger := mockLogger.NewMockLogger(ctrl)

    // ユースケースのモック化
    mockService := mockService.NewMockService(ctrl)

    t.Run("正常終了すること", func(t *testing.T) {
        // モック設定
        mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
        mockService.EXPECT().Execute(gomock.Any()).Return(nil)

        // DIコンテナ設定
        container := di.NewContainer(db, mockLogger)

        // ハンドラー設定
        handler := NewHealthcheckHandler(container, mockService)

        // テスト実行
        res, err := handler.Healthcheck(
            context.Background(),
            gen.HealthcheckRequestObject{},
        )

        // レスポンス結果の変換
        jsonRes, ok := res.(gen.Healthcheck200JSONResponse)

        // 検証
        assert.NoError(t, err)
        assert.True(t, ok)
        assert.Equal(t, "OK", jsonRes.Message)
    })

    t.Run("異常終了すること", func(t *testing.T) {
        // モック設定
        mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
        mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
        mockService.EXPECT().Execute(gomock.Any()).Return(assert.AnError)

        // DIコンテナ設定
        container := di.NewContainer(db, mockLogger)

        // ハンドラー設定
        handler := NewHealthcheckHandler(container, mockService)

        // テスト実行
        res, err := handler.Healthcheck(
            context.Background(),
            gen.HealthcheckRequestObject{},
        )

        // レスポンス結果の変換
        jsonRes, ok := res.(gen.Healthcheck500JSONResponse)

        // 検証
        assert.NoError(t, err)
        assert.True(t, ok)
        assert.Equal(t, "Internal Server Error", jsonRes.Message)
    })
}

 

・「src/internal/presentation/handler/handler_v1.go」

package handler

import (
    "context"

    "go-oapi-aidd/internal/di"
    "go-oapi-aidd/internal/presentation/gen"
    "go-oapi-aidd/internal/presentation/handler/supporting/healthcheck"
)

type HandlerV1 struct {
    container *di.Container
}

func NewHandlerV1(
    container *di.Container,
) *HandlerV1 {
    return &HandlerV1{
        container: container,
    }
}

func (h *HandlerV1) Healthcheck(
    ctx context.Context,
    request gen.HealthcheckRequestObject,
) (gen.HealthcheckResponseObject, error) {
    healthcheckHandler := healthcheck.NewHealthcheckHandler(h.container, h.container.HealthcheckService)
    return healthcheckHandler.Healthcheck(ctx, request)
}

※これはDI用にハンドラーをまとめるファイルです。

 

次にルーターファイを作成するため、以下のコマンドを実行します。

$ mkdir -p src/internal/presentation/router && touch src/internal/presentation/router/router.go

 

次に作成したファイルを以下のように記述します。

・「src/internal/presentation/router/router.go」

package router

import (
    "encoding/json"
    "net/http"
    "os"
    "strconv"
    "time"

    "github.com/getkin/kin-openapi/openapi3"
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "github.com/go-chi/cors"
    nethttpMiddleware "github.com/oapi-codegen/nethttp-middleware"

    "go-oapi-aidd/internal/di"
    "go-oapi-aidd/internal/presentation/gen"
    "go-oapi-aidd/internal/presentation/handler"
)

// リクエストのタイムアウト設定値取得関数
func getRequestTimeout() time.Duration {
    // 環境変数「REQUEST_TIMEOUT_SECONDS」の値を取得
    requestTimeoutSecondsStr := os.Getenv("REQUEST_TIMEOUT_SECONDS")
    if requestTimeoutSecondsStr == "" {
        requestTimeoutSecondsStr = "10"
    }

    // INT型に変換
    requestTimeoutSecondsInt, err := strconv.Atoi(requestTimeoutSecondsStr)
    if err != nil {
        panic(err)
    }

    // Duration型の秒数で返す
    return time.Duration(requestTimeoutSecondsInt) * time.Second
}

// 許可されたオリジン設定値取得関数
func getAllowedOrigins() []string {
    // 環境変数「CORS_ALLOWED_ORIGIN」の値を取得
    allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN")
    if allowedOrigin == "" {
        allowedOrigin = "*"
    }

    return []string{
        allowedOrigin,
    }
}

// OpenAPIを利用したバリデーションチェック追加用関数
func addOapiValidation(path string, r chi.Router) {
    // Swagger(OpenAPI定義)取得
    swagger, err := gen.GetSwagger()
    if err != nil {
        panic(err)
    }

    // Servers定義を対象のpathで上書き
    swagger.Servers = openapi3.Servers{
        &openapi3.Server{
            URL: path,
        },
    }

    // バリデーション設定
    validator := nethttpMiddleware.OapiRequestValidatorWithOptions(
        swagger,
        &nethttpMiddleware.Options{
            SilenceServersWarning: true,
            ErrorHandler: func(w http.ResponseWriter, message string, statusCode int) {
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(statusCode)
                _ = json.NewEncoder(w).Encode(map[string]any{
                    "message": message,
                })
            },
        },
    )

    // ミドルウェアにvalidatorを追加
    r.Use(validator)
}

func NewRouter(container *di.Container) *chi.Mux {
    r := chi.NewRouter()

    // ミドルウェアの設定
    r.Use(middleware.RequestID)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(getRequestTimeout()))
    r.Use(cors.Handler(cors.Options{
    AllowedOrigins: getAllowedOrigins(),
    AllowedMethods: []string{
        "GET",
        "POST",
        "PUT",
        "PATCH",
        "DELETE",
        "OPTIONS",
    },
    AllowedHeaders: []string{
        "Accept",
        "Authorization",
        "Content-Type",
    },
        AllowCredentials: true,
        MaxAge: 300,
    }))
    // テスト実行時以外にロガーを追加
    if env := os.Getenv("ENV"); env != "testing" {
        r.Use(middleware.Logger)
    }

    // 「/api/v1」のルーティング設定
    handlerV1 := handler.NewHandlerV1(container)
    strictHandlerV1 := gen.NewStrictHandler(handlerV1, nil)
    apiV1 := "/api/v1"
    r.Route(apiV1, func(r chi.Router) {
        addOapiValidation(apiV1, r)
        gen.HandlerFromMux(strictHandlerV1, r)
    })

    return r
}

 

次にファイル「src/main.go」を以下のように修正します。

package main

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "os"
    "time"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    
    "go-oapi-aidd/internal/di"
    "go-oapi-aidd/internal/infrastructure/database"
    "go-oapi-aidd/internal/infrastructure/logger"
    "go-oapi-aidd/internal/infrastructure/observability"
    "go-oapi-aidd/internal/presentation/router"
)

func main() {
    // コンテキスト設定
    ctx := context.Background()

    // OpenTelemetryのトレース出力先設定
    otelExporterType := os.Getenv("OTEL_EXPORTER_TYPE")
    shutdownTracer := observability.InitTracer(ctx, otelExporterType)
    defer func() {
        _ = shutdownTracer(context.Background())
    }()

    // DB取得
    db, err := database.NewBunDB()
    if err != nil {
        panic("failed to connect database")
    }
    defer db.Close()

    // ロガー取得
    logger := logger.NewSlogLogger()

    // DIコンテナ取得
    container := di.NewContainer(db, logger)

    // ルーティング設定の取得
    r := router.NewRouter(container)

    otelHandler := otelhttp.NewHandler(
        r,
        "go-oapi-aidd",
        otelhttp.WithSpanNameFormatter(func(operation string, req *http.Request) string {
            return req.Method + " " + req.URL.Path
        }),
    )

    // ポート番号の設定
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    startPort := fmt.Sprintf(":%s", port)

    // サーバー設定
    srv := &http.Server{
        Addr:              startPort,
        Handler:           otelHandler,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       30 * time.Second,
        WriteTimeout:      30 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    // サーバー起動
    logger.Info(false, ctx, "start server go-oapi-aidd")
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        logger.Error(false, ctx, err.Error())
        os.Exit(1)
    }
}

 

Dockerコンテナの再ビルドと起動

次に以下のコマンドを実行し、Dockerコンテナの再ビルドをします。

$ docker compose run --rm api go mod tidy
$ docker compose run --rm api golangci-lint fmt -v ./...
$ docker compose build --no-cache

 

次に以下のコマンドを実行し、Dockerコンテナを起動します。

$ docker compose up -d

 

次に以下のコマンドを実行し、Dockerコンテナのステータスを確認します。

$ docker compose ps

 

コマンド実行後、以下のようにAPIコンテナとDBコンテナが起動すればOKです。

 

ユニットテストの実行

次に以下のコマンドを実行し、ユニットテストを実行します。

$ docker compose exec api env ENV=testing go test -v -tags=unit $(docker compose exec api env ENV=testing go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=unit ./...)

 

テスト実行後、以下のように全てのテストがPASSすればOKです。

 

インテグレーションテストの作成と実行

次にインテグレーションテスト用のファイルを作成するため、以下のコマンドを実行します。

$ mkdir -p src/tests/integration/supporting/healthcheck && touch src/tests/integration/supporting/healthcheck/healthcheck_test.go

 

次に作成したファイルを以下のように記述します。

・「src/tests/integration/supporting/healthcheck/healthcheck_test.go」

//go:build integration

package healthcheck

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"

    "go-oapi-aidd/internal/di"
    "go-oapi-aidd/internal/infrastructure/database"
    "go-oapi-aidd/internal/infrastructure/logger"
    "go-oapi-aidd/internal/presentation/router"
)

func TestHealthcheck_OK(t *testing.T) {
    // DB取得
    db, err := database.NewBunDB()
    if err != nil {
        panic("failed to connect database")
    }
    defer db.Close()

    // ロガー取得
    logger := logger.NewSlogLogger()

    // DIコンテナ取得
    container := di.NewContainer(db, logger)

    // ルーティング設定の取得
    r := router.NewRouter(container)

    // リクエスト作成
    req := httptest.NewRequest(http.MethodGet, "/api/v1/healthcheck", nil)
    res := httptest.NewRecorder()

    // テスト実行
    r.ServeHTTP(res, req)

    // 検証
    assert.Equal(t, http.StatusOK, res.Code)
    assert.JSONEq(t, `{"message":"OK"}`, res.Body.String())
}

 

次に以下のコマンドを実行し、インテグレーションテストを実行します。

$ docker compose exec api env ENV=testing go test -v -tags=integration $(docker compose exec api env ENV=testing go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=integration ./...)

 

テスト実行後、以下のように全てのテストがPASSすればOKです。

 

Healthcheck(ヘルスチェック)APIをPostmanで試す

次に作成したHealthcheck(ヘルスチェック)APIをPostmanで試します。

GETメソッドで「http://localhost:8080/api/v1/healthcheck」を実行し、以下のように正常終了して想定通りの結果になればOKです。

 

スポンサーリンク

OpenAI「Codex」におけるハーネスエンジニアリング設定

今回はAI駆動開発を前提としていますが、そのためには何らかのAIツールを利用し、かつ『ハーネスエンジニアリング』も重要になります。

この記事では、AIツールにOpenAIの「Codexアプリ」(または「Codex CLI」)を利用する前提とし、そのツールに合わせてハーネス設計を行います。

 

ハーネスエンジニアリングとは?

AIツールにおける『ハーネスエンジニアリング』とは、AIモデルやAIエージェントを安全かつ安定して動作させるため、入力・出力の制御やルール設定、評価・テストの仕組みなどを設計・構築する技術のことです。

これによってAIの挙動を管理し、品質や再現性を担保した状態で実運用できるようにします。

 

OpenAI「Codex」におけるハーネス設計について

OpenAI「Codex」でハーネス設計をしたい場合は、以下のようなディレクトリ構成で各種ファイルを作成し、それぞれ内容を定義していくことになります。

/my-project
 |
 ├── /.codex
 |    |
 |    ├── config.toml ※codexに関する設定
 |    |
 |    ├── /rules ※sandbox外で実行できるコマンド制御の設定
 |    |    |
 |    |    └── default.rules
 |    |
 |    ├── /agents ※サブエージェントを利用する場合の例
 |    |    |
 |    |    ├── orchestrator.toml ※指揮者(全体制御)
 |    |    |
 |    |    ├── tester.toml ※テスター(テストコード作成・検証)
 |    |    |
 |    |    ├── implementer.toml ※実装者(テストコードを通すように実装)
 |    |    |
 |    |    └── reviewer.toml ※レビュワー(設計や品質チェック)
 |    |
 |    ├── /workflows ※ワークフローを利用する場合の例
 |    |    |
 |    |    └── tdd_flow.md ※TDD(テスト駆動開発)のフロー
 |    |
 |    └── /skills ※Agent Skillsを利用する場合
 |
 ├── AGENTS.md ※ルートディレクトリ用(共通ルール)
 |
 ├── /src
 |    └── AGENTS.md ※サブディレクトリ用(専用ルール)
 |
 └── /docs ※詳細仕様にファイル分割して格納(各種AGENTS.mdから指定する)
      |
      └── /rules
           |
           ├── architecture.md ※設計思想のルール定義
           |
           ├── database.md ※DB設計のルール定義
           |
           ├── sub.md ※サブディレクトリ用のルール定義
           |
           └── testing.md ※テスト用のルール定義

 

codexの設定ファイルを作成

まずcodexの設定ファイルを作成するため、以下のコマンドを実行してファイルを作成します。

$ mkdir .codex && touch .codex/config.toml

 

次に作成したファイルを以下のように記述します。

・「.codex/config.toml」

model = "gpt-5.2"
model_reasoning_effort = "medium"

sandbox_mode = "workspace-write"

※今回はモデルに「gpt-5.2」を指定(利用中のプランで使用可能なものを指定して下さい)、モデルの推論設定に「medium(バランス型)」を指定、サンドボックスモードに「workspace-write(書き込み許可)」を指定

 

尚、Codexアプリなどで対象のプロジェクトに設定した「.codex/config.toml」を有効化するには、対象のプロジェクトを信頼するプロジェクトにする必要があります。

対象のプロジェクトを信頼するプロジェクトにしたい場合は、グローバル設定の方のconfig.toml(~/.codex/config.toml)で、以下のような設定を追加して下さい。

[projects."対象のプロジェクトのフルパス"]
trust_level = "trusted"

※設定変更を反映するにはCodexアプリの再起動が必要です。

 

sandbox外のコマンド制御設定を追加

次にsandbox外で実行できるコマンドを制御するため、以下のコマンドを実行してファイルを作成します。

$ mkdir -p .codex/rules && touch .codex/rules/default.rules

 

次に作成したファイルを以下のように記述します。

・「.codex/rules/default.rules」

# --- ディスク破壊(即禁止) ---
prefix_rule(
    pattern=["mkfs", "dd if=", "wipefs"],
    decision="forbidden",
    justification="ストレージ破壊操作"
)

# --- Git履歴破壊(即禁止) ---
prefix_rule(
    pattern=["git push --force", "git push -f", "git push --force-with-lease"],
    decision="forbidden",
    justification="履歴改変(強制push)によるリポジトリ破壊の可能性"
)

# --- GitHub CLI 危険操作 ---
prefix_rule(
    pattern=["gh repo delete"],
    decision="forbidden",
    justification="リポジトリ削除(不可逆)"
)

prefix_rule(
    pattern=["gh pr merge --admin"],
    decision="prompt",
    justification="保護ルール無視の強制マージ"
)

prefix_rule(
    pattern=["gh workflow run"],
    decision="prompt",
    justification="CI/CDの強制実行"
)

prefix_rule(
    pattern=["gh secret set", "gh secret delete"],
    decision="prompt",
    justification="機密情報の変更"
)

# --- 危険削除(確認) ---
prefix_rule(
    pattern=["rm -rf", "sudo rm"],
    decision="prompt",
    justification="不可逆削除の可能性"
)

# --- 権限昇格 ---
prefix_rule(
    pattern=["sudo"],
    decision="prompt",
    justification="システム影響が大きい"
)

※まずは危険なコマンドを制御する

 

agents(サブエージェント)機能を追加

次にagents(サブエージェント)機能も使えるようにする想定として、以下のコマンドを実行してファイルを作成します。

$ mkdir -p .codex/agents
$ touch .codex/agents/pm.toml .codex/agents/tester.toml .codex/agents/implementer.toml .codex/agents/reviewer.toml

※エージェント機能を使いたい場合は、命令の中に明示的に「エージェントを利用する旨」を入れて下さい。また、エージェント機能はトークン消費が激しくなる可能性があるため、必要に応じて利用して下さい。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「.codex/agents/pm.toml」(プロダクトマネージャー / 指揮者)

name = "pm"
description = "全体の進行管理とタスク分解を行うプロダクトマネージャー"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたは開発全体を指揮するプロダクトマネージャーです。

役割:
- 要件を整理し、タスクに分解する
- API開発ではOpenAPI仕様を確定し、RED開始前にコード生成まで完了させる
- tester → reviewer → implementer → reviewer の順でタスクを割り振る
- 各エージェントのアウトプットを評価し、次の行動を決める

ルール:
- 必ずTDDサイクル(RED → GREEN → REVIEW)を守る
- API仕様の確定は実装ではなく、REDより前の仕様定義として扱う
- 不完全な実装は次に進めない
- 問題があれば前の工程に差し戻す

出力形式:
- 次に行動するエージェント名
- 依頼内容(具体的に)
"""

 

・「.codex/agents/tester.toml」(テスター)

name = "tester"
description = "テストコード作成と仕様検証を行うTDDエンジニア"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたはTDDに従うテストエンジニアです。

役割:
- 仕様に基づいたテストコードを書く(RED)
- API仕様の抜けや期待仕様の不足を検出する

ルール:
- まず失敗するテストを書く(RED)
- テストは仕様を正確に表現すること
- エッジケースも考慮する
- 実装コードは書かない
- GREENフェーズの実装レビューは行わない

出力:
- テストコード
- テストの意図説明
"""

 

・「.codex/agents/implementer.toml」(実装者)

name = "implementer"
description = "テストを通すための実装を行うエンジニア"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたはTDDに従う実装エンジニアです。

役割:
- テストが通る最小限の実装を書く

ルール:
- REDレビューが完了してから実装を開始する
- `src/AGENTS.md` と `docs/rules` に従う
- テストを通すことを最優先
- 過剰な設計をしない(YAGNI)
- テストを書かない
- 生成コードは手動編集しない
- リファクタは必要最低限

出力:
- 実装コード
- 実装の簡単な説明
"""

 

・「.codex/agents/reviewer.toml」(レビュワー)

name = "reviewer"
description = "仕様適合性・品質・セキュリティをレビューするエンジニア"
model_reasoning_effort = "high"
sandbox_mode = "read-only"

developer_instructions = """
あなたはコードレビュー担当です。

役割:
- 実装が仕様を満たしているか確認
- バグ・セキュリティ問題を検出
- テスト不足を指摘
- REDレビューではテストの妥当性を確認する
- GREENレビューでは実装とテスト・OpenAPIの整合性を確認する

観点:
- 正しさ(仕様通りか)
- セキュリティ
- 境界値・異常系
- テスト網羅性

ルール:
- 指摘は具体的に
- 再現手順を書く
- スタイル指摘は重要な場合のみ
- 実装修正は行わない

出力:
- 指摘一覧(重要度付き)
- 修正提案
"""

 

workflows(ワークフロー)機能を追加

次にworkflows(ワークフロー)機能を使えるようにするため、以下のコマンドを実行してファイルを作成します。

$ mkdir -p .codex/workflows && touch .codex/workflows/tdd_flow.md

※今回はTDD開発を前提とします。

 

次に作成したファイルを以下のように記述します。

・「.codex/workflows/tdd_flow.md」

# TDD開発フロー

本フローは pm の指示を起点として進行するが、各エージェントは自身の責務に従い独立して実行する。

本プロジェクトは「OpenAPI駆動 + TDD(Red → Green → Refactor)」を前提とする。

## 1. 開発フロー

1. pmが要件を整理し、タスクとして分解する
2. API開発の場合はpmがOpenAPI定義を確定し、コード生成まで完了させる
3. testerが失敗するテストを書く(RED)
4. reviewerがテスト内容をレビューする(REDレビュー)
5. implementerがテストを通す(GREEN)
6. reviewerが実装をレビューする(GREENレビュー)
7. 問題があれば修正ループに戻る

## 2. API開発時の追加フロー(重要)

API開発の場合はREDテスト作成より前に以下を必ず行う。
このフェーズはAPI仕様の確定であり、実装とは扱わない。

### ① OpenAPI定義の更新

- `src/openapi/openapi.yaml`
- `src/openapi/paths/*`
- `src/openapi/components/schemas/*`

ここでAPI仕様を確定させる(実装より優先)。

### ② コード生成

OpenAPI更新後は必ず生成する。

```bash
make generate
```

生成コード:

- `src/internal/presentation/gen`

※生成コードは編集禁止

## 3. テストの責務ルール

### testerの責務

- RED(失敗するテスト)を先に書く
- API仕様の抜けを検出する
- handler / usecase / service の境界を明確化する

### テストのモックルール

- handler → usecase / service は必ずmock化する
- usecase → repository は必ずmock化する
- 外部APIは必ずmock化する
- DBアクセスの検証はintegration testで行う
- handler testでDIコンテナ生成にDBインスタンスが必要な場合でも、DBアクセスそのものは行わない

## 4. 実装ルール(GREEN)

実装に関する詳細ルールは以下を参照する:

- `src/AGENTS.md`

## 5. レビュー(reviewer)

reviewerは以下を検査する:

### REDレビュー

- テストの妥当性
- 仕様の過不足
- モックの適切性
- API設計の整合性

### GREENレビュー

- 依存ルール違反
- レイヤー逸脱
- 実装の妥当性
- テストとの一致
- OpenAPIとの整合性

## 6. ルール

- REDなしで実装しない
- REDレビューなしでGREENに進まない
- GREEN未達でレビューしない
- GREENレビューなしで完了しない
- レビューNGなら再実装
- OpenAPI未定義のAPI実装は禁止
- 生成コードは編集禁止
- DBアクセスをunit testに持ち込まない
- 外部APIの実通信は禁止

## 7. 判断ルール

判断に迷った場合は `docs/rules` にある以下に関するドキュメントを参照する:

- アーキテクチャ設計のルール定義
- モジュール分類のルール定義
- 依存関係のルール定義

それでも不明な場合は、モジュール分類のルール定義の判断不能時ルールに従い、最も軽い構造を優先する:

generic → supporting → core → shared

 

Agent Skillsを追加

次にAgent Skillsも使えるようにするため、以下のスキルをそれぞれ追加します。

 

スキル「plan-to-issue」:プランモードで作成した開発計画をGitHubのIssue用のフォーマットへ変換して自動登録する

まずは以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir -p .codex/skills/plan-to-issue && touch .codex/skills/plan-to-issue/SKILL.md
$ mkdir -p .codex/skills/plan-to-issue/references && touch .codex/skills/plan-to-issue/references/rules.md
$ mkdir -p .codex/skills/plan-to-issue/assets && touch .codex/skills/plan-to-issue/assets/template.md
$ mkdir -p .codex/skills/plan-to-issue/scripts && touch .codex/skills/plan-to-issue/scripts/create_issue.sh .codex/skills/plan-to-issue/scripts/update_issue.sh
$ chmod +x .codex/skills/plan-to-issue/scripts/create_issue.sh .codex/skills/plan-to-issue/scripts/update_issue.sh

※スクリプトは「chmod +x」で実行権限を付与します。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「.codex/skills/plan-to-issue/SKILL.md」

---
name: plan-to-issue
description: プランモードで作成した開発計画をGitHubのIssue用のフォーマットへ変換して自動登録する。必要に応じて登録後のIssue内容の修正もする。
---

# plan-to-issue

## 概要

このスキルは、プランモードで作成された開発計画を読み込み、指定されたルールとテンプレートに従ってGitHub Issue用のフォーマットへ変換し、専用のスクリプトを用いてGitHubへ自動登録します。

また、必要に応じて登録後のIssue内容の修正も行うことができます。

## 重要原則(最重要)

- summary(概要)は要約してよい
- tasks(タスク)は**絶対に要約してはならない(展開必須)**
- tasksは「実装可能な手順レベル」にまで分解する
- 不明点は補完しつつも、実行手順を失わないことを優先する

## 参照ファイル

このスキルを実行する際は、以下のファイルを必ず読み込んで使用してください。

- **変換ルール定義**: `references/rules.md`
  - 開発計画からIssueフォーマットへ変換する際のルールや抽出条件が記載されています。
- **出力フォーマット**: `assets/template.md`
  - Issueの本文を作成するためのMarkdownテンプレートです。
- **Issueの新規登録スクリプト**: `scripts/create_issue.sh`
  - 作成したIssueデータをGitHubに登録するためのシェルスクリプトです。
- **Issueの更新スクリプト**: `scripts/update_issue.sh`
  - 登録後のIssue内容を更新するためのシェルスクリプトです。

## 入力

### 1. Issueを新規作成する場合

- プランモードで生成された開発計画(必須)
- ラベル名(任意)

### 2. 登録済みIssueを更新する場合

- issue_number(直前に新規登録したIssueの番号が必須)
- 修正した開発計画(必須)
- ラベル名(任意)

## 出力

新規登録または更新したGitHub Issue

## 実行手順(ワークフロー)

### 1. Issueを新規作成する場合

以下のステップに従って処理を実行してください。

1. **開発計画の解析**

  - 目的・背景・機能・制約を分解して理解する
  - 暗黙的なタスクも抽出する

2. **変換ルールの読み込み**

  - `references/rules.md` を厳密に適用する

3. **テンプレート適用**

  - `assets/template.md` に従いIssue構造を生成

4. **Issueのタイトルを作成**

  - `references/rules.md` の命名規則に従う

5. **GitHubのIssueを登録**

  - `scripts/create_issue.sh` を使用してIssue作成
  - 任意のラベル名が指定された場合、ラベル名も合わせて登録

### 2. 登録済みIssueを更新する場合

以下のステップに従って処理を実行してください。

1. **開発計画の解析**

  - 目的・背景・機能・制約を分解して理解する
  - 暗黙的なタスクも抽出する

2. **変換ルールの読み込み**

  - `references/rules.md` を厳密に適用する

3. **テンプレート適用**

  - `assets/template.md` に従いIssue構造を生成

4. **GitHubのIssueを更新**

  - `scripts/update_issue.sh` を利用し、対象のissue_numberに紐づくIssueの本文を更新します。
  - 任意のラベル名が指定された場合、ラベル名も合わせて登録します。

## 品質保証(必須チェック)

- tasksは抽象化されていないか
- 実装手順が復元可能か
- 設計・実装・テストが含まれているか

 

・「.codex/skills/plan-to-issue/references/rules.md」

# 変換ルール定義(plan-to-issue)

## 1. 基本方針

- 1 Issue = 1目的(または1成果物)
- 情報は構造化するが、**実行可能性を失わないことを最優先**
- summaryとtasksは役割を完全に分離する

### 最重要ルール

summaryとtasksの役割分離

| 項目 | 性質 | ルール |
|------|------|--------|
| summary | 要約 | 圧縮OK |
| tasks | 実行手順 | 圧縮禁止・完全展開必須 |

## 2. フィールド対応ルール

### 2-1 summary(概要)

- 目的ベースで1〜3文
- 実装詳細は書かない
- 要約OK

### 2-2 background(背景)

- 課題・理由・現状を記述
- 不足は文脈補完可

### 2-3 scope(スコープ)

- In Scope / Out of Scope を明示
- 暗黙スコープは必ず明文化

### 2-4 tasks

#### 絶対ルール

禁止:

- 「実装する」
- 「対応する」
- 「作る」
- 「改善する」

→ これらはすべて禁止(抽象語)

---

#### 必須ルール

tasksは必ず以下を満たす:

##### 1. 手順分解必須

- 設計 → 実装 → テスト → 検証の順で展開

##### 2. 最小粒度

- 1タスク = 30〜90分単位
- 1タスク = 単一アクション

##### 3. 具体動詞必須

- 「作成する」「定義する」「追加する」「検証する」

##### 4. 必須分解カテゴリ

必ず以下を含める:

- 仕様確認
- 設計
- データ構造定義
- 実装
- エラーハンドリング
- テスト
- 動作確認

##### 5. タスク展開ルール(強化)

開発計画からタスク化する際は:

- 機能を「手順」に分解
- 暗黙工程も明示化
- 一つの機能を1タスクにしない

##### 6. 例

悪い例:

- APIを実装する

良い例:

- API仕様(リクエスト/レスポンス)を定義する
- エンドポイントルーティングを作成する
- バリデーション処理を実装する
- DBアクセス層を実装する
- エラーハンドリングを追加する
- 単体テストを作成する
- 結合テストを実行する

---

### 2-5 acceptance_criteria

- 「できること」で記述
- 必ずテスト可能であること

例:

- 正常リクエストで200が返ること
- 不正入力でエラーが返ること


### 2-6 notes

- 技術制約
- 依存関係
- 未確定事項(要確認)
- 将来対応

## 3. タイトル生成ルール

Issueタイトルは以下のルールで生成する

### フォーマット

[種別] 内容の要約

### 種別の分類

- feat: ユーザーに価値を提供する新機能
- fix: 不具合の修正
- refactor: 挙動を変えない内部改善
- perf: 性能改善を主目的とした修正
- docs: ドキュメントの追加・更新
- test: テストの追加・修正
- infra: インフラ・CI/CD・環境構築
- chore: 上記に当てはまらない雑務(極力使わない)

### ルール

- 30〜60文字程度に収める
- 内容が一目で分かるようにする
- summaryの内容をベースに生成する

例:

- feat: ユーザー登録APIを実装する
- fix: ログイン時の認証エラーを修正

---

## 4. Issue分割ルール

- 10タスク以上 → 分割必須
- 独立機能は別Issue
- 依存関係はnotesへ

---

## 5. 不足情報補完

- 合理的推測OK
- 推測はnotesに明記
- 不確定は「要確認」

---

## 6. 禁止事項

- 抽象タスク
- 実行不能なタスク
- tasksの要約
- 背景なしIssue

---

## 7. 出力品質チェック

- tasksが完全手順化されている
- 抽象名詞が存在しない
- 設計〜テストが揃っている
- 実装順が復元可能

 

・「.codex/skills/plan-to-issue/assets/template.md」

## 概要
{{summary}}

## 背景
{{background}}

## スコープ
{{scope}}

## タスク
- [ ] {{tasks}}

## 受け入れ条件
- {{acceptance_criteria}}

## 補足
{{notes}}

 

・「.codex/skills/plan-to-issue/scripts/create_issue.sh」

#!/usr/bin/env bash

set -euo pipefail

# ==================================================
# Usage:
# ./create_issue.sh "タイトル" "本文" "ラベル名"
# ==================================================

TITLE="${1:-}"
BODY="${2:-}"
LABELS="${3:-}"

# タイトルと本文の入力チェック
if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo "Usage: $0 \"タイトル\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &> /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &> /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "Issueを作成中..."

# Issue作成
if [[ -n "$LABELS" ]]; then
  gh issue create \
    --title "$TITLE" \
    --body-file <(printf "%s" "$BODY") \
    --label "$LABELS"
else
  gh issue create \
    --title "$TITLE" \
    --body-file <(printf "%s" "$BODY")
fi

echo "Issue作成完了"

 

・「.codex/skills/plan-to-issue/scripts/update_issue.sh」

#!/usr/bin/env bash

set -euo pipefail

# ==================================================
# Usage:
# ./update_issue.sh "Issue番号" "本文" "ラベル名"
# ==================================================

ISSUE_NUMBER="${1:-}"
BODY="${2:-}"
LABELS="${3:-}"

# Issue番号と本文の入力チェック
if [[ -z "$ISSUE_NUMBER" || -z "$BODY" ]]; then
  echo "Usage: $0 \"Issue番号\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &> /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &> /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "Issueを更新中..."

# Issue更新
if [[ -n "$LABELS" ]]; then
  gh issue edit "$ISSUE_NUMBER" \
    --body-file <(printf "%s" "$BODY") \
    --add-label "$LABELS"
else
  gh issue edit "$ISSUE_NUMBER" \
    --body-file <(printf "%s" "$BODY")
fi

echo "Issue更新完了"

 

スキル「auto-commit」:修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する

次に以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir -p .codex/skills/auto-commit && touch .codex/skills/auto-commit/SKILL.md

 

次に作成したファイルをそれぞれ以下のように記述します。

・「.codex/skills/auto-commit/SKILL.md」

---
name: auto-commit
description: 修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する
---

# auto-commit

## 概要
このスキルは、コード修正後の未ステージ状態から変更を検出し、自動でステージング、差分解析、コミットメッセージ生成、git commit実行までを一貫して行う。

## 処理フロー

### 1. 未ステージの変更をステージ

以下のコマンドを実行し、未ステージの変更をすべてステージする。

```bash
git add -A
```

---

### 2. ステージ済み差分の取得

以下のコマンドを実行し、ステージ済みの変更内容の差分情報を取得する。

```bash
git diff --cached
```

---

### 3. 差分解析

取得したコードの差分情報から以下を解析する:

- 変更の目的(機能追加 / バグ修正 / リファクタリング / 雑務)
- 影響範囲(ファイル・モジュール)
- ユーザー視点での変化
- 変更が単一責務かどうか

---

### 4. コミットメッセージ生成

コードの差分情報の解析結果をもとに、
Conventional Commits形式でコミットメッセージを生成する。

#### フォーマット

`<type>: <summary>`

#### type一覧

- test(red): TDDで先に追加する失敗テストコード
- test: 通常のテストコードの追加・修正
- feat: 新規機能追加
- fix: バグ修正
- refactor: 振る舞いを変えないコード改善・リファクタリング
- perf: 性能改善を主目的とした修正
- docs: ドキュメント修正
- chore: その他

#### コミットメッセージの例

- test(red): 有効期限切れトークンの失敗テストを追加
- test: 新規アカウント作成のインテグレーションテストを追加
- feat: ログイン機能を追加
- fix: データの重複登録バグの修正
- refactor: 重複コードを共通化
- perf: 全てのデータ取得SQLの発行回数を削減
- docs: READMEの更新
- chore: GitHub Actions設定を整理

---

### 5. コミット実行

生成したコミットメッセージを用いて以下のコマンドを実行し、コミットを実行する。

```bash
git commit -m "<generated commit message>"
```

---

## 動作ルール

### 0. 実行順序

- 処理は必ず「git add → diff確認 → commit可否判定」の順で実行する
- git add は処理の最初に一度だけ実行する(追加実行は禁止)

---

### 1. 変更検知・終了条件

- 変更が存在しない場合は、git add / commit は実行せず処理を終了する
- git diff --cached の結果が空の場合も同様にコミットを行わない
- git add 後に再度差分を確認し、変更がない場合は即終了する
- 空コミットは絶対に作成しない

---

### 2. 安全性チェック(危険変更の制御)

- .env, secrets, credential系ファイルの変更が含まれる場合は必ず停止する
- 破壊的変更(大量削除・ファイル削除が多数)の場合は警告を出し、停止する

- 破壊的変更の定義:
  - 削除ファイルが5件以上
  - または差分行数の削除が追加の2倍以上

- 上記条件を満たす場合は自動実行せず、必ず停止してユーザー確認を求める

---

### 3. コミット構造ルール

- 1つのコミットは必ず単一の目的(単一責務)になるようにする
  - 複数の変更目的が混在している場合は論理的にまとめるか警告扱いとする

- 大規模変更(複数モジュールにまたがる変更)の場合は、可能であれば分割コミットを優先する

---

### 4. コミットメッセージ生成ルール

- コミットメッセージは必ず変更内容に基づいて生成し、推測や一般論で補完しない
  - 差分から読み取れない情報は含めない

- メッセージは簡潔にしつつ、変更の「意図」が分かる表現にする
  - 実装内容の羅列ではなく、何が改善されたかを優先する

- コミットメッセージは50〜72文字程度を目安にする

- 英語・日本語のどちらを使うかはプロジェクトの既存コミットに合わせて統一する

---

### 5. 変更分類ルール

- 変更内容が以下に該当する場合は chore に分類する
  - フォーマット修正のみ
  - コメント修正のみ
  - 空白・改行整理のみ
  - 自動生成ファイルの更新

---

### 6. コミット実行ルール

- commit 実行前に最終的なメッセージを内部で確定させること
- git commit は確定したメッセージのみで実行する

---

## まとめ

このスキルは以下を保証する:

- 安全な自動コミット(事故防止)
- 意味ベースのコミット生成
- 単一責務の維持
- 変更意図の明確化
- チーム規約との整合性

 

スキル「tdd-draft-pr」:Issueと追加したREDテストをもとに、テストレビュー用のドラフトPRを作成する

次に以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir -p .codex/skills/tdd-draft-pr && touch .codex/skills/tdd-draft-pr/SKILL.md
$ mkdir -p .codex/skills/tdd-draft-pr/assets && touch .codex/skills/tdd-draft-pr/assets/template.md
$ mkdir -p .codex/skills/tdd-draft-pr/scripts && touch .codex/skills/tdd-draft-pr/scripts/create_draft_pr.sh
$ chmod +x .codex/skills/tdd-draft-pr/scripts/create_draft_pr.sh

※スクリプトは「chmod +x」で実行権限を付与します。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「.codex/skills/tdd-draft-pr/SKILL.md」

---
name: tdd-draft-pr
description: Issueと追加したREDテストをもとに、テストレビュー用のドラフトPRを作成する
---

# tdd-draft-pr

## 概要

このスキルはTDD(テスト駆動開発)のRedフェーズで利用します。

Issueに定義された受け入れ条件をもとに追加したテストコードを解析し、テストレビュー用のドラフトPRを作成します。

レビュー対象は実装ではなく、テストシナリオおよび受け入れ条件のカバレッジです。

## 参照ファイル

このスキルを実行する際は、以下のファイルを必ず読み込んで使用してください。

- ドラフトPRテンプレート: `assets/template.md`
- ドラフトPR作成スクリプト: `scripts/create_draft_pr.sh`

## 入力

- 対応Issue
- 直前のコミット情報
- 変更されたテストコード
- ラベル名(任意)

## 出力

GitHubのドラフトPR

## 実行手順

### 1. Issue情報を取得する

Issue番号およびIssue本文を取得する。

Issueに記載されている以下の情報を確認する。

- 背景
- 要件
- 受け入れ条件(Acceptance Criteria)

---

### 2. 直前のコミットと変更内容を取得する

```bash
git log -1 --pretty=format:"%h%n%s%n%b"
git show --no-color
```

確認対象:

- 追加・変更されたテストコード
- テスト名
- テストシナリオ
- テスト対象機能

確認ルール:

- テストコード以外の変更はレビュー対象外とする
- TDDのREDフェーズのコミットであることを確認する
- REDフェーズのコミットでない場合は処理を中断する

---

### 3. テストシナリオを生成する

Issueの受け入れ条件と追加されたテストコードを対応付けながら、レビュー対象となるテストシナリオを抽出する。

生成ルール:

- テスト実装の詳細は記載しない
- テストコードから意図を読み取り自然言語化する
- 受け入れ条件との対応が分かる粒度にする
- チェックリスト形式で出力する

例:

```text
- [ ] 有効なメールアドレスの場合は登録できる
- [ ] 不正なメールアドレスの場合は登録できない
- [ ] メールアドレス未入力の場合は登録できない
```

生成したチェックリスト形式のテストシナリオ一覧を `tests` として保持する。

---

### 4. ドラフトPR本文を生成する

`assets/template.md` を読み込み、以下を置換する。

- `{{issue}}`
  - 対応Issue
  - フォーマット: `#<issue_number>`

- `{{tests}}`
  - 事前に生成したテストシナリオ一覧 `tests`

---

### 5. ドラフトPRタイトルを生成する

フォーマット:

```text
auto: <Issueタイトル>
```

Issueタイトルを要約せず、そのまま利用する。

---

### 6. ドラフトPRを作成する

#### 6-1. 現在のブランチをGitHubへプッシュ

```bash
git push -u origin HEAD --no-verify
```

補足:

- Git hooks はスキップする。
- pushに失敗した場合は処理を中断する。

#### 6-2. ドラフトPR作成

- `scripts/create_draft_pr.sh` を利用してドラフトPRを作成する
- 任意のラベルが指定された場合は付与する

---

## 補足ルール

- Issueを仕様の唯一のソースとする
- Issueの内容とテストコードの両方を参照してテストシナリオを生成する
- テストコードを必ず参照する
- テストコードから推測できない内容を書かない
- 実装内容の説明は記載しない
- レビュー対象はテストシナリオと受け入れ条件のカバレッジである
- TDDステータスは常に Redフェーズ とする

 

・「.codex/skills/tdd-draft-pr/assets/template.md」

## 対応Issue

{{issue}}

## 追加したテストシナリオ

{{tests}}

## レビュー観点

- Issueの受け入れ条件を十分にカバーできているか
- テストシナリオに不足がないか
- 境界値・異常系のテストシナリオが十分に含まれているか

## TDDステータス

Redフェーズ

実装はまだ行っておらず、失敗するテストのみを追加しています。
レビュー完了後に実装を追加し、Ready for Review に変更します。

 

・「.codex/skills/tdd-draft-pr/scripts/create_draft_pr.sh」

#!/usr/bin/env bash

set -euo pipefail

# ==================================================
# Usage:
# ./create_draft_pr.sh "タイトル" "本文" "ラベル名"
# ==================================================

TITLE="${1:-}"
BODY="${2:-}"
LABELS="${3:-}"

# タイトルと本文の入力チェック
if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo "Usage: $0 \"タイトル\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &> /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &> /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "ドラフトPRを作成中..."

# ドラフトPR作成
if [[ -n "$LABELS" ]]; then
  gh pr create \
    --draft \
    --title "$TITLE" \
    --body-file <(printf "%s" "$BODY") \
    --label "$LABELS"
else
  gh pr create \
    --draft \
    --title "$TITLE" \
    --body-file <(printf "%s" "$BODY")
fi

echo "ドラフトPR作成完了"

 

スキル「pr-sync-comment」:直前のコミット内容をもとにPRコメントを生成し、ブランチをpushした上でPRにコメントを追加する

次に以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir -p .codex/skills/pr-sync-comment && touch .codex/skills/pr-sync-comment/SKILL.md
$ mkdir -p .codex/skills/pr-sync-comment/assets && touch .codex/skills/pr-sync-comment/assets/template.md
$ mkdir -p .codex/skills/pr-sync-comment/scripts && touch .codex/skills/pr-sync-comment/scripts/add_pr_comment.sh
$ chmod +x .codex/skills/pr-sync-comment/scripts/add_pr_comment.sh

※スクリプトは「chmod +x」で実行権限を付与します。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「.codex/skills/pr-sync-comment/SKILL.md」

---
name: pr-sync-comment
description: 直前のコミット内容をもとにPRコメントを生成し、ブランチをpushした上でPRにコメントを追加する
---

# pr-sync-comment

## 概要

このスキルは、直前のコミット内容をPRに説明として同期するためのスキルです。

コミット済みの変更をブランチへpushし、その変更内容についてPRコメントとして投稿します。

## 入力

- 対応するPR番号
- 直前のコミット情報

## 出力

- ブランチ更新・PRコメント追加

## 実行手順

### 1. 直前のコミット情報を取得する

```bash
git log -1 --pretty=format:"%h%n%s%n%b"
git show --no-color
```

確認対象:

- 変更されたコード
- 追加・削除されたロジック
- コミットメッセージの意図

※ 差分ベースでのみ判断し、推測で補完しない

---

### 2. 変更内容を要約する

直前コミットの変更内容をPRコメント用に要約する。

生成ルール:

- 事実ベースで記述する
- 実装詳細ではなく変更の意図に寄せる
- 箇条書きで出力する
- レビュー判断は含めない

例:

```text
- ユーザー登録処理にバリデーション追加
- メールアドレス形式チェックを追加
- null入力時の分岐を修正
```

生成した内容を `changes` として保持する。

---

### 3. PRコメントを生成する

`assets/template.md` を読み込み、以下を置換する:

- `{{changes}}`
  - 変更内容の要約(箇条書き)

---

### 4. PRを更新する

#### 4-1. 現在のブランチをGitHubへプッシュ

```bash
git push -u origin HEAD
```

失敗した場合は処理を中断する。

---

#### 4-2. PRへコメントを追加

- `scripts/add_pr_comment.sh` を利用してPRにコメントを追加する

## 補足ルール

- 直前のコミットのみを対象とする
- 複数コミットの統合説明はしない
- 実装評価・レビュー判断は行わない
- PRの履歴として追いやすいことを優先する

 

・「.codex/skills/pr-sync-comment/assets/template.md」

## 変更内容

{{changes}}

## 補足

- 本コメントは直前のコミット内容をもとに自動生成されています

 

・「.codex/skills/pr-sync-comment/scripts/add_pr_comment.sh」

#!/usr/bin/env bash

set -euo pipefail

# ==================================================
# Usage:
# ./add_pr_comment.sh "PR番号" "追加コメント内容"
# ==================================================

PR_NUMBER="${1:-}"
BODY="${2:-}"

# PR番号と追加コメント内容の入力チェック
if [[ -z "$PR_NUMBER" || -z "$BODY" ]]; then
  echo "Usage: $0 \"PR番号\" \"追加コメント内容\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &> /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &> /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "PRにコメントを投稿中..."

# PRにコメントを投稿
gh pr comment "$PR_NUMBER" --body-file <(printf "%s" "$BODY")

echo "PRにコメントを投稿しました。"

 

スキル「tdd-ready-pr」:レビュー済みのドラフトPRに実装レビューコメントを追加し、PRをReady for Reviewへ変更する

次に以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir -p .codex/skills/tdd-ready-pr && touch .codex/skills/tdd-ready-pr/SKILL.md
$ mkdir -p .codex/skills/tdd-ready-pr/assets && touch .codex/skills/tdd-ready-pr/assets/template.md
$ mkdir -p .codex/skills/tdd-ready-pr/scripts && touch .codex/skills/tdd-ready-pr/scripts/mark_pr_ready.sh
$ chmod +x .codex/skills/tdd-ready-pr/scripts/mark_pr_ready.sh

※スクリプトは「chmod +x」で実行権限を付与します。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「.codex/skills/tdd-ready-pr/SKILL.md」

---
name: tdd-ready-pr
description: レビュー済みのドラフトPRに実装レビューコメントを追加し、PRをReady for Reviewへ変更する
---

# tdd-ready-pr

## 概要

このスキルはTDD(テスト駆動開発)のGreenフェーズで利用します。

レビュー済みのドラフトPRに対して実装レビュー用コメントを追加し、PRをReady for Reviewへ変更します。

レビュー対象は実装内容であり、テストの正しさはREDフェーズで担保済みです。

## 参照ファイル

- レビューコメントテンプレート: `assets/template.md`
- PR更新スクリプト: `scripts/mark_pr_ready.sh`

## 入力

- 対応するドラフトPR(PR番号)
- 直前のコミット情報

## 出力

- コメント追加済みPR
- Ready for Review状態のPR

## 実行手順

### 1. ドラフトPR情報を取得する

ドラフトPRから以下を取得する:

- PR本文
- 対応Issue
- REDフェーズで追加されたテスト内容

※ テストの正当性は変更しない(REDフェーズでレビュー済み前提)

---

### 2. 直前のコミットと変更内容を取得する

```bash
git log -1 --pretty=format:"%h%n%s%n%b"
git show --no-color
```

確認対象:

- テストを満たすために追加された実装コード
- 変更された既存ロジック
- リファクタリング箇所

確認ルール:

- テストを満たすための変更のみ対象とする
- スタイル変更・関係ない修正は除外する

---

### 3. 実装内容を生成する

Greenフェーズで追加・変更した実装の要約を生成する。

生成ルール:

- テストを満たすための変更に限定する
- 設計説明ではなく変更内容の要約とする
- 箇条書きで出力する

例:

```text
- ユーザー登録時のバリデーション追加
- メールアドレス形式チェックの実装
- 登録処理への条件分岐追加
```

生成した内容を `implementations` として保持する。

---

### 4. テストの確認結果を生成する

テスト実行結果のサマリを生成する。

生成ルール:

- REDフェーズで追加されたテストの成功確認
- 既存テストの回帰確認
- CI相当の確認結果

例:

```text
- REDテスト: OK
- 既存テスト: OK
- CI: OK
```

生成した内容を `checks` として保持する。

---

### 5. Greenフェーズレビューコメントを生成する

`assets/template.md` を読み込み、以下を置換する:

- `{{implementations}}` → 実装内容
- `{{checks}}` → テスト確認結果

---

### 6. PRを更新する

#### 6-1. 現在のブランチをGitHubへプッシュ

```bash
git push -u origin HEAD
```

補足:

- 失敗した場合は処理を中断する。

---

#### 6-2. 実装レビュー依頼用コメント追加 + Ready化

- scripts/mark_pr_ready.sh を利用してPRを更新する
- コメント追加とReady for Review化を実行する

## 補足ルール

- このスキルは実装レビュー専用である
- テスト設計・仕様レビューは対象外
- REDフェーズのレビュー結果を前提とする
- 実装とテストの整合性のみを確認する

 

・「.codex/skills/tdd-ready-pr/assets/template.md」

## TDDのGreenフェーズ完了

REDフェーズで追加したテストに対して実装を追加し、すべてのテストが成功することを確認しました。

### 実装内容

{{implementations}}

### テストの確認結果

{{checks}}

### レビュー観点

- 実装がテストの意図を満たしているか
- 不要な実装や過剰な一般化がないか
- テストを維持したまま改善できるリファクタリング余地があるか

レビューをお願いします。

 

・「.codex/skills/tdd-ready-pr/scripts/mark_pr_ready.sh」

#!/usr/bin/env bash

set -euo pipefail

# ==================================================
# Usage:
# ./mark_pr_ready.sh "PR番号" "追加コメント内容"
# ==================================================

PR_NUMBER="${1:-}"
BODY="${2:-}"

# PR番号と追加コメント内容の入力チェック
if [[ -z "$PR_NUMBER" || -z "$BODY" ]]; then
  echo "Usage: $0 \"PR番号\" \"追加コメント内容\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &> /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &> /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "ドラフトPRにコメントを投稿し、ready for review に変更中..."

# ドラフトPRにコメントを投稿
gh pr comment "$PR_NUMBER" --body-file <(printf "%s" "$BODY")

# ドラフトPRを ready for review 状態に変更
gh pr ready "${PR_NUMBER}"

echo "PRを更新しました。"

 

今回のプロジェクト用のハーネス設定

次に今回のプロジェクト用のハーネス設定を行いますが、今回はOpenAPI定義からGoのコード生成およびDDD(ドメイン駆動設計)ベースのディレクトリ構成かつ、TDD(Test Driven Development)を採用したやり方で開発をすることを想定しています。

そのため、今回は以下のようなディレクトリ構成を想定しています。

/go-oapi-adii
 |
 └── /src
      |
      ├── /cmd
      |    |
      |    ├── /migrate/main.go(DBのマイグレーション用スクリプト)
      |    |
      |    └── /seed/main.go(マスタデータ登録用スクリプト)
      |
      ├── /internal
      |    |
      |    ├── /core(中核の業務領域)
      |    |    |
      |    |    └── /[domain_name](ドメインモジュール)
      |    |         |
      |    |         ├── /domain(ドメイン)
      |    |         |   |
      |    |         |   ├── [entity_name].go(エンティティ)
      |    |         |   |
      |    |         |   ├── [valueobject_name].go(値オブジェクト)
      |    |         |   |
      |    |         |   ├── [domain_service_name]_service.go(ドメインサービス)
      |    |         |   |
      |    |         |   ├── [repository_name]_repository.go(リポジトリのインターフェース)
      |    |         |   |
      |    |         |   └── [gateway_name]_gateway.go(外部サービス用のインターフェース)
      |    |         |
      |    |         ├── /usecase(ユースケース層)
      |    |         |
      |    |         └── /infrastructure(ドメイン用のインフラストラクチャ層)
      |    |              |
      |    |              ├── /repository(リポジトリの実装)
      |    |              |    |
      |    |              |    ├── /command(書き込み)
      |    |              |    |
      |    |              |    └── /query(読み込み)
      |    |              |
      |    |              └── /external(外部サービスの実装)
      |    |
      |    ├── /supporting(補完的なの業務領域)
      |    |    |
    |    |    └── /[supporting_name](サービス層)
      |    |         |
      |    |         └── service.go
      |    |
      |    ├── /generic(一般的な業務領域)
      |    |
      |    ├── /shared(横断関心)
      |    |
      |    ├── /di(DIコンテナ層)
      |    |    |
      |    |    └── container.go
      |    |
      |    ├── /infrastructure(共通インフラストラクチャ層)
      |    |    |
      |    |    ├── /database(データベース設定)
      |    |    |    |
      |    |    |    ├── /schema(Bun用のスキーマ定義)
      |    |    |    |
      |    |    |    ├── /seed(マスタデータ登録用のseed定義)
      |    |    |    |
      |    |    |    └── bun.go (ORM「Bun」の接続定義)
      |    |    |
      |    |    ├── /logger(共通ロガーの実装)
      |    |    |
      |    |    ├── /migrations(Bunのマイグレーション用SQL・スクリプト)
      |    |    |
      |    |    └── /observability(トレース取得用の設定など)
      |    |
      |    └── /presentation(プレゼンテーション層)
      |         |
      |         ├── /gen(OpenAPI定義から生成したGoコード)
      |         |
      |         ├── /handler(ハンドラー層)
      |         |
      |         └── /router(ルーター設定)
      |
      ├── /openapi(OpenAPIの定義)
      |    |
      |    ├── /components/schemas(コンポーネントのスキーマ定義)
      |    |
      |    ├── /paths(各種APIの定義)
      |    |
      |    └── openapi.yaml
      |
      └── /tests(インテグレーションテスト・e2eテスト用)

 

そして、テスト駆動開発(TDD:Test-Driven Development)については、プログラムの機能実装前に対応するテストコードを先に作成し、そのテストに合格するように実装とリファクタリングを繰り返す開発手法です。

今回はそれらを踏まえて各種定義ファイルを作成していく必要があるため、まずは以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir -p docs/rules
$ touch docs/rules/architecture.md docs/rules/module-classification.md docs/rules/dependency-rules.md
$ touch docs/rules/orm-bun.md docs/rules/oapi-codegen.md docs/rules/testing.md
$ touch src/AGENTS.md AGENTS.md

※今回はできるだけ最小構成になるようにしています。ファイル数を増やしてしっかり定義した方が、より想定外の動作やリスクを防ぐことができるようになります。

 

・「docs/rules/architecture.md」(アーキテクチャ設計のルール定義)

# アーキテクチャ設計のルール定義

このファイルは本プロジェクトにおける「構造の意味」を定義します。

コードの配置判断や依存ルールは扱いません。それらはそれぞれ以下に委譲:

- `docs/rules/module-classification.md`(配置判断)
- `docs/rules/dependency-rules.md`(依存関係)

## 1. 本プロジェクトのディレクトリ構成

```
/my-project
 |
 └── /src
      |
      ├── /cmd
      |    |
      |    ├── /migrate/main.go(DBのマイグレーション用スクリプト)
      |    |
      |    └── /seed/main.go(マスタデータ登録用スクリプト)
      |
      ├── /internal
      |    |
      |    ├── /core(中核の業務領域)
      |    |    |
      |    |    └── /[domain_name](ドメインモジュール)
      |    |         |
      |    |         ├── /domain(ドメイン)
      |    |         |   |
      |    |         |   ├── [entity_name].go(エンティティ)
      |    |         |   |
      |    |         |   ├── [valueobject_name].go(値オブジェクト)
      |    |         |   |
      |    |         |   ├── [domain_service_name]_service.go(ドメインサービス)
      |    |         |   |
      |    |         |   ├── [repository_name]_repository.go(リポジトリのインターフェース)
      |    |         |   |
      |    |         |   └── [gateway_name]_gateway.go(外部サービス用のインターフェース)
      |    |         |
      |    |         ├── /usecase(ユースケース層)
      |    |         |
      |    |         └── /infrastructure(ドメイン用のインフラストラクチャ層)
      |    |              |
      |    |              ├── /repository(リポジトリの実装)
      |    |              |    |
      |    |              |    ├── /command(書き込み)
      |    |              |    |
      |    |              |    └── /query(読み込み)
      |    |              |
      |    |              └── /external(外部サービスの実装)
      |    |
      |    ├── /supporting(補完的なの業務領域)
      |    |    |
    |    |    └── /[supporting_name](サービス層)
      |    |         |
      |    |         └── service.go
      |    |
      |    ├── /generic(一般的な業務領域)
      |    |
      |    ├── /shared(横断関心)
      |    |
      |    ├── /di(DIコンテナ層)
      |    |    |
      |    |    └── container.go
      |    |
      |    ├── /infrastructure(共通インフラストラクチャ層)
      |    |    |
      |    |    ├── /database(データベース設定)
      |    |    |    |
      |    |    |    ├── /schema(Bun用のスキーマ定義)
      |    |    |    |
      |    |    |    ├── /seed(マスタデータ登録用のseed定義)
      |    |    |    |
      |    |    |    └── bun.go (ORM「Bun」の接続定義)
      |    |    |
      |    |    ├── /logger(共通ロガーの実装)
      |    |    |
      |    |    ├── /migrations(Bunのマイグレーション用SQL・スクリプト)
      |    |    |
      |    |    └── /observability(トレース取得用の設定など)
      |    |
      |    └── /presentation(プレゼンテーション層)
      |         |
      |         ├── /gen(OpenAPI定義から生成したGoコード)
      |         |
      |         ├── /handler(ハンドラー層)
      |         |
      |         └── /router(ルーター設定)
      |
      ├── /openapi(OpenAPIの定義)
      |    |
      |    ├── /components/schemas(コンポーネントのスキーマ定義)
      |    |
      |    ├── /paths(各種APIの定義)
      |    |
      |    └── openapi.yaml
      |
      └── /tests(インテグレーションテスト・e2eテスト用)
```

## 2. アーキテクチャの基本思想

本プロジェクトは以下を前提として設計される:

- OpenAPI定義からのGoコード生成(oapi-codegen)
- TDD(Test Driven Development)の徹底
- ドメイン志向のモジュール分割
- 依存関係を厳密に制御した制約ベースアーキテクチャ

本プロジェクトは「DDDの概念を一部取り入れた構造」であるが、
DDDの厳密なレイヤー構造・依存ルールには従わない。

## 3. 設計の位置づけ

本アーキテクチャは以下の中間的性質を持つ:

- DDDのように業務中心の構造を持つ
- ただし依存制御はDDDではなくルールベース
- Clean Architectureのようなレイヤー依存は持たない
- AI生成に最適化された単純構造を採用する

## 4. モジュールの役割定義

### core(中核の業務領域)

システムのビジネス価値そのもの。

- 業務ルールを持つ
- 状態変化を扱う
- 複数ステップの整合性を保証する
- 最も重要な制約を持つ

---

### supporting(補完的な業務領域)

単一業務フローを処理する領域。

- トランザクションスクリプト型
- 手続き的処理
- 軽量な業務ロジック
- ドメイン分割不要

---

### generic(一般的な業務領域)

業務意味を持たない処理。

- CRUD
- データ変換
- 文字列・時間・ID処理
- 純粋な技術関数

---

### shared(横断関心・共通基盤)

全モジュール共通の基盤。

- ログ
- エラー定義
- DTO
- ユーティリティ
- sharedは全モジュールから利用可能な唯一の共通基盤
- sharedは「業務ロジックを持たない純粋な共通部品のみ」を配置する
- sharedは「依存の終端」であり、上位概念を持たない

---

## 5. 設計思想

本アーキテクチャの目的は以下である:

- 責務の明確な分離
- 判断の機械化
- AI生成の安定化
- 構造の単純化

## 6. レイヤーの考え方

本プロジェクトでは「レイヤー」ではなく「責務領域」として扱う。

- core = 業務の中心
- supporting = 業務フロー
- generic = 技術処理
- shared = 共通基盤

機能の重要度の上下関係はあるが、依存関係の上下構造は存在しない。

## 7. 非目標

本設計は以下を目的としない:

- 厳密なDDDの再現
- Clean Architectureの完全準拠
- 柔軟性の最大化

## 8. 判断の委譲

詳細な判断は以下に委譲する:

- 配置 → `docs/rules/module-classification.md`
- 依存 → `docs/rules/dependency-rules.md`

## 9. 判断基準(概要)

- 業務の中心 → core
- 業務フロー → supporting
- 技術処理 → generic
- 横断基盤 → shared

 

・「docs/rules/module-classification.md」(モジュール分類のルール定義)

# モジュール分類のルール定義

このファイルは「どこにコードを配置するか」を決定する唯一の判断ルールです。

APIを作成する際は、まず以下の4種類のいずれかに分類し、それに応じた配置先ディレクトリを決定します。

- 中核の業務領域 → core
- 補完的な業務領域 → supporting
- 一般的な業務領域 → generic
- 横断関心・共通基盤 → shared

また、依存関係・設計構造については一切扱いません。 
(architecture.md / dependency-rules.mdに委譲)

## 1. 分類定義

### 中核の業務領域(core)

ビジネスの中核ロジックを扱う領域。

#### 特徴

- 業務ルールが存在する
- 状態変化を伴う
- 複数ステップの整合性が必要
- システムの中心価値を表す

#### 例

- ユーザー登録
- 決済処理
- 注文確定
- 権限管理

### 補完的な業務領域(supporting)

補助的な業務処理を扱う領域(transaction script型)。

#### 特徴

- 単一の業務フロー
- ドメイン分割不要
- 手続き的に完結する
- 軽いビジネスロジックを含む

#### 例

- ヘルスチェック
- メール送信フロー
- バッチ処理
- 外部API連携の単純処理

### 一般的な業務領域(generic)

軽量なデータ操作・ユーティリティ領域。

#### 特徴

- CRUDレベルの処理
- ビジネスルールを持たない
- 再利用可能な純粋な関数
- 技術的処理のみ

#### 例

- UUID生成
- 文字列操作
- 日付フォーマット
- 単純なDB CRUD(業務意味なし)

### 横断関心・共通基盤(shared)

複数モジュールから利用される共通要素を扱う領域。

#### 特徴

- 複数モジュールから利用される
- ビジネスロジックを持たない
- システム全体で共通利用される

#### 例

- 共通ロガーのインターフェースの定義
- 共通ユーティリティの定義

## 2. 配置判断ルール

### core に入れる条件

- ビジネスルールがある
- 状態が変化する
- 複数処理の整合性が必要

### supporting に入れる条件

- 単一の業務フロー
- 手続き的処理
- ドメイン分割不要

### generic に入れる条件

- CRUDのみ
- ロジックなし
- 技術的処理
- 再利用関数

### shared に入れる条件

- 複数モジュールから使われる
- 共通定義である
- 他の3分類に属さない横断要素

## 3. 優先ルール(重要)

判断が重複する場合は以下を優先する:

1. core(最も業務意味が強いもの)
2. supporting(業務フロー)
3. generic(最も軽い処理)
4. shared(横断・共通)

## 4. 具体例

| 機能 | 分類 |
|------|------|
| 注文処理 | core |
| メール送信 | supporting |
| データの単純CRUD | generic |
| 共通ロガーのインターフェースの定義 | shared |

## 5. 判断不能時ルール

どの分類か迷った場合は以下の順で優先する:

generic → supporting → core → shared

ただし共通性が明確な場合は shared を優先する。

## 6. 禁止事項

- 複数分類への同時所属は禁止
- 推測でcoreに昇格させない
- supportingをcore代替にしない
- genericに業務ロジックを入れない
- sharedにビジネスロジックを入れない

 

・「docs/rules/dependency-rules.md」(依存関係のルール定義)

# 依存関係のルール定義

このファイルは「モジュール間の依存関係(importルール)」を定義します。

設計思想ではなく、実装時の強制ルールとして扱うこと。

## 1. 基本原則

本プロジェクトは完全分離構造を採用します。

- `internal/core`
- `internal/supporting`
- `internal/generic`
- `internal/shared`

これらは明確に分離される。

## 2. 依存の基本ルール

### 許可される依存

- `internal/core` → `internal/shared` のみ
- `internal/supporting` → `internal/shared` のみ
- `internal/generic` → `internal/shared` のみ

### internal/shared の特性

- `internal/shared` は他に依存してはいけない(完全独立)
- `internal/shared` の本番コードは外部パッケージへの依存を禁止する
  - 使用可能なのはGo標準ライブラリのみ
  - 例:context / errors / fmt / time など
- `internal/shared` は全モジュールから利用可能

### テスト用コードの例外

- `_test.go` と `mock_*` 配下の生成mockはテスト支援コードとして扱う
- テスト支援コードは gomock などのテスト用外部パッケージに依存してよい
- テスト支援コードを本番コードからimportしてはならない

## 3. モジュール間依存ルール

### internal/core

- 他のモジュール(supporting/generic)には依存しない
- `internal/shared` のみ利用可能

### internal/supporting

- 他のモジュール(core/generic)には依存しない
- `internal/shared` のみ利用可能

### internal/generic

- 他のモジュール(core/supporting)には依存しない
- `internal/shared` のみ利用可能

### internal/shared

- いかなるモジュールにも依存してはいけない
- 完全に独立した共通基盤である

## 4. 禁止事項(重要)

- `internal/core` / `internal/supporting` / `internal/generic` 間のimportは禁止
- `internal/shared` 以外への依存は禁止
- 循環依存は禁止
- ビジネスロジックをsharedに書くことは禁止
- infrastructure的実装を `internal/shared` に入れることは禁止

## 5. 依存構造の最終形

依存関係は以下の形のみ許可される:

- `internal/core` → `internal/shared`
- `internal/supporting` → `internal/shared`
- `internal/generic` → `internal/shared`
- `internal/shared` → どこにも依存しない(本番コード)

## 6. 設計意図

このルールの目的は以下である:

- モジュール間の結合を完全に排除する
- 依存関係の事故を防ぐ
- AI生成時の構造崩壊を防ぐ
- 予測可能なコード生成を保証する

## 7. 判断基準

迷った場合は以下を優先する:

- 依存を持たないことを優先する
- sharedへの依存のみ許可する
- 他モジュール参照が必要な場合は設計を見直す

## 8. 例

### OK

- `internal/core` → `internal/shared` のみimportして利用可能(shared配下すべて対象)
- `internal/supporting` → `internal/shared` のみimportして利用可能(shared配下すべて対象)
- `internal/generic` → `internal/shared` のみimportして利用可能(shared配下すべて対象)
- `internal/shared` → どこにも依存しない

### NG

- `internal/core` → `internal/supporting`
- `internal/supporting` → `internal/generic`
- `internal/generic` → `internal/core`

 

・「docs/rules/orm-bun.md」(ORM「Bun」に関するルール定義)

# ORM「Bun」に関するルール定義

このファイルは ORM「Bun」の利用ルールを定義する。

Bun の利用可能場所・利用禁止場所・マイグレーション運用を定義し、DB アクセスの責務を明確化する。

## 1. 基本方針

本プロジェクトの DB アクセスは Bun を使用する。

SQL アクセスは Bun を経由して実装すること。

Bun の利用場所は明示的に制限される。

モジュール分類ごとに異なる利用ルールを適用する。

- core:Repository パターン
- supporting:Transaction Script
- generic:Active Record

詳細なモジュール分類は `module-classification.md` を参照すること。

## 2. Bun の利用可能場所

Bun は以下でのみ使用できる。

- `internal/core/*/infrastructure/repository`
- `internal/core/*/domain/*_repository.go` の repository interface 引数型(`bun.IDB` のみ)
- `internal/core/*/usecase` の DB 実行主体保持・repository 呼び出し引数(`bun.IDB` のみ)
- `internal/supporting`
- `internal/generic`
- `internal/infrastructure/database`
- `internal/di`

## 3. Bun の利用禁止場所

以下では Bun を使用してはならない。

- `internal/core/*/domain`
  - 例外:repository interface の引数型として `bun.IDB` を使う場合のみ許可
- `internal/core/*/usecase`
  - 例外:DB 実行主体として `bun.IDB` を保持し、repository 呼び出し時に引数として渡す場合のみ許可
- `internal/core/*/infrastructure/external`
- `internal/shared`
- `internal/presentation`

## 4. core での利用ルール

core は DDD ベースの構造を採用する。

### domain

domain は業務ルールのみを扱う。

禁止事項:

- Bun の利用
- SQL の記述
- Bun のタグ利用
- Bun の型への依存
  - 例外:repository interface の引数型として `bun.IDB` を使う場合のみ許可
- DB アクセス

許可事項:

- repository interface では、通常の `*bun.DB` と `bun.Tx` の両方を受け取れるようにする目的で、引数型に `bun.IDB` を使ってよい。
- `bun.IDB` は DB 実行主体を表す型としてのみ扱い、domain 内で query builder の生成、SQL記述、DBアクセスを行ってはならない。

例:

```go
type MemberRepository interface {
FindByID(ctx context.Context, db bun.IDB, id string) (*Member, error)
}
```

### usecase

usecase は業務フローのみを扱う。

禁止事項:

- Bun の利用
  - 例外:DB 実行主体として `bun.IDB` を保持し、repository 呼び出し時に引数として渡す場合のみ許可
- SQL の記述
- DB への直接アクセス
- query builder の生成

許可事項:

- usecase がトランザクション境界を扱うために、通常の `*bun.DB` と `bun.Tx` の両方を受け取れる `bun.IDB` を保持してよい。
- usecase は `bun.IDB` を repository に渡すだけに留め、SQL記述、query builder生成、DBアクセス処理を行ってはならない。

例:

```go
type CalculatePointUsecase struct {
    db bun.IDB
    repository domain.MemberQueryRepository
}

func (u *CalculatePointUsecase) Execute(ctx context.Context, input Input) (Output, error) {
    member, err := u.repository.FindByID(ctx, u.db, input.MemberID)
    // ...
}
```

### infrastructure/repository

DB アクセスは repository が担当する。

Bun は repository 実装内で利用する。

対象:

- command(書き込み)
- query(読み取り)

Repository は domain に定義されたインターフェースを実装する。

### infrastructure/external

外部サービス連携を担当する。

禁止事項:

- Bun の利用
- DB アクセス

---

## 5. supporting での利用ルール

supporting は Transaction Script を採用する。

DB アクセスが必要な場合は `service.go` で Bun を利用してよい。

ただし以下を守ること。

- 単一業務フローとして完結させる
- 業務処理と DB 処理を過度に混在させない
- 不要に複雑な SQL を記述しない
- ドメインモデルを導入しない

## 6. generic での利用ルール

generic は Active Record を採用する。

単純 CRUD については Bun を直接利用してよい。

対象:

- Create
- Read
- Update
- Delete

禁止事項:

- 複雑な業務ルールの実装
- ドメインロジックの実装
- 複数ステップの整合性制御
- 長大なトランザクション処理

generic は技術的処理のみを扱う。

## 7. スキーマ定義

Bun用のスキーマ定義は `internal/infrastructure/database/schema` 配下に配置する。

責務:

- テーブル定義とのマッピング
- Bun タグの定義

禁止事項:

- 業務ロジックの実装
- バリデーションの実装
- ドメインモデルとの兼用

スキーマ定義は永続化専用モデルとして扱う。

## 8. 接続定義

Bunの接続設定は `internal/infrastructure/database/bun.go` を利用する。

責務:

- DB 接続生成
- 接続設定
- Bun 初期化

禁止事項:

- 業務ロジックの実装
- Repository の生成

## 9. DI コンテナ

DI コンテナは Bun を利用してよい。

配置:

- `internal/di/container.go`

責務:

- Bun 接続の注入
- Repository の生成
- Service の生成
- Handler の生成
- 依存関係の組み立て

禁止事項:

- 業務ロジックの実装
- SQL の記述

## 10. マイグレーション

マイグレーション関連ファイルは `internal/infrastructure/migration` 配下に配置する。

マイグレーションは必ず Bun のマイグレーション機能を利用する。

### ファイル作成ルール

マイグレーションファイルは手動作成してはならない。

必ず以下のコマンドを実行する。

```bash
docker compose exec api go run cmd/migrate/main.go create_sql [ファイル名]
```

例:

```bash
docker compose exec api go run cmd/migrate/main.go create_sql create_users_table
```

実行すると以下のような2つのファイルが生成される。

```text
internal/infrastructure/migrations/20260119023405_create_users_table.up.sql
internal/infrastructure/migrations/20260119023405_create_users_table.down.sql
```

- `*.up.sql`:マイグレーション実行用
- `*.down.sql`:ロールバック用

### 命名規則

ファイル名は以下の形式とする。

```text
[操作]_[対象]
```

例:

- `create_users_table`
- `add_index_to_orders`
- `drop_legacy_columns`

タイムスタンプは自動付与されるため手動指定しない。

### 初期化

マイグレーション管理テーブルを作成する場合は以下を実行する。

```bash
docker compose exec api go run cmd/migrate/main.go init
```

テスト用 DB に対して実行する場合:

```bash
docker compose exec api env ENV=testing go run cmd/migrate/main.go init
```

### 状態確認

現在のマイグレーション状態を確認する場合は以下を実行する。

```bash
docker compose exec api go run cmd/migrate/main.go status
```

### マイグレーション実行

マイグレーションを適用する場合は以下を実行する。

```bash
docker compose exec api go run cmd/migrate/main.go migrate
```

一度に実行された SQL は同一グループとして管理される。

### ロールバック

直前のマイグレーショングループをロールバックする場合は以下を実行する。

```bash
docker compose exec api go run cmd/migrate/main.go rollback
```

ロールバックはファイル単位ではなくグループ単位で実行される。

### マイグレーション作成ルール

- `up.sql` と `down.sql` は必ず対で作成する
- `down.sql` を省略してはならない
- 既存の適用済みマイグレーションを書き換えてはならない
- 過去のマイグレーションを削除してはならない
- スキーマ変更は新規マイグレーションとして追加する

## 11. 禁止事項

以下を禁止する。

- domain で Bun を使用すること
- domain で SQL を記述すること
- usecase で Bun を使用すること
- usecase で SQL を記述すること
- presentation で Bun を使用すること
- shared で Bun を使用すること
- DI コンテナに業務ロジックを書くこと
- Bun のスキーマを業務モデルとして扱うこと
- マイグレーションファイルを手動作成すること
- 適用済みマイグレーションを書き換えること

## 12. 判断基準

DB アクセスが必要な場合は以下で判断する。

### core

Repository に実装する。

### supporting

`service.go` に実装する。

### generic

Active Record として実装する。

判断に迷った場合は `module-classification.md` を参照し、対象機能の分類を先に決定すること。

 

・「docs/rules/oapi-codegen.md」(oapi-codegenに関するルール定義)

# oapi-codegenに関するルール定義

このファイルはOpenAPI定義およびoapi-codegenに関するルールを定義する。

## 1. 基本方針

本プロジェクトではAPI実装より先にOpenAPIを定義する。

API仕様を唯一の正とし、GoコードはOpenAPIから生成する。

実装からAPI仕様を作成してはならない。

## 2. OpenAPI First

新規API追加・既存API変更時は以下の順序を厳守する。

1. OpenAPI定義を修正する
2. コード生成を実行する
3. テストコード(Red)を作成する
4. 実装する(Green)
5. リファクタリングする(Refactor)

## 3. OpenAPI定義の配置

API定義は以下に配置する。

- `src/openapi/openapi.yaml`
- `src/openapi/paths/*`
- `src/openapi/components/schemas/*`

API仕様は必ずOpenAPI上で管理する。

## 4. コード生成

OpenAPI定義変更後は必ず以下を実行する。

```bash
make generate
```

生成コードは以下に出力される。

- `src/internal/presentation/gen`

## 5. 生成コードの扱い

生成コードは編集してはならない。

禁止事項:

- hand edit
- コメント追加
- メソッド追加
- 構造体修正
- import追加

変更が必要な場合は OpenAPI 定義を修正し再生成する。

## 6. 使用範囲

生成コードは presentation 層でのみ利用する。

利用可能:

- `src/internal/presentation/handler`
- `src/internal/presentation/router`

利用不可:

- `src/internal/core`
- `src/internal/supporting`
- `src/internal/generic`
- `src/internal/shared`

## 7. DTOルール

生成される request / response は DTO として扱う。

DTOは外部境界専用であり、業務モデルとして利用してはならない。

## 8. ドメインモデルとの分離

DTOと業務モデルは分離する。

禁止事項:

- DTOを永続化モデルとして利用する
- DTOを業務モデルとして利用する
- DTOをそのまま内部処理へ渡す

handler層で変換を行うこと。

## 9. handlerの責務

handlerは以下のみを担当する。

- リクエスト受信
- DTO変換
- usecase / service呼び出し
- レスポンス変換

業務ロジックを書いてはならない。

## 10. 判断基準

API変更が必要な場合は必ず以下を確認する。

- OpenAPIを先に変更したか
- make generate を実行したか
- 生成コードを編集していないか

迷った場合はOpenAPI定義を正として扱う。

 

・「docs/rules/testing.md」(テストのルール定義)

# テストのルール定義

本ファイルはテストの分類・責務・モック方針を定義する。
TDD(Red → Green → Refactor)を前提とする。

## 1. 基本方針

- 必ずTDDで開発する(Red → Green → Refactor)
- 実装より先にテストを書く(Red)
- テストは仕様の定義として扱う
- 外部依存をテストに持ち込まない設計を優先する

## 2. テスト分類

### unit test

最小単位のロジック検証。

対象:
- domain
- usecase
- supporting service
- genericの純粋関数・単純処理

特徴:
- DBアクセスなし
- 外部APIなし
- 高速実行
- 純粋ロジック中心

### integration test

複数コンポーネントを結合したテスト。

対象:
- repository
- infrastructure
- DBアクセスを含む処理
- HTTPリクエストからDBアクセスまでを含むAPI結合

特徴:
- ローカルDockerのDBを使用する
- 実DBでSQL・ORM(Bun)を検証する
- 本番と同等のルーティング構成を通して実際のHTTPリクエストを送ってよい
- OpenAPI validator / generated router / handler / usecase / repository / DB の結合を検証してよい
- HTTPリクエストのバリデーション検証はintegration testで扱ってよい
- 外部APIは使用しない(必ずモック化)

### e2e test

API全体の動作確認。

対象:
- 実行中のアプリケーションプロセスに対するHTTP API全体

特徴:
- 実行中のサーバーへ実際のHTTPリクエストを送る
- DBはDocker環境を使用
- 外部APIは必ずモック化

※ただしe2eは必須ではなく、重要な業務フローのみ対象とする

## 3. モックルール

### 共通ルール

- 外部APIは必ずモック化する
- テストの安定性を優先する

### repositoryモック

- unit testではrepositoryは必ずmock化する
- usecaseテストではDBアクセスを排除する

### usecaseモック(重要)

- handlerテストではusecaseを必ずmock化する
- supportingのhandlerテストではserviceを必ずmock化する
- handlerテストはhandlerメソッドを直接呼び出し、request object / response object の変換責務に限定する

## 4. テスト配置ルール

### unit test

- domain/usecase/service/generic処理と同階層、または近傍に配置
  - 例:
    - `xxx_usecase_test.go`
    - `xxx_service_test.go`

### integration test

- `tests/integration`

### e2e test

- `tests/e2e`

## 5. handlerテストの特別ルール

handlerテストでは以下を必須とする:

- usecase / serviceは必ずモック化する
- handlerメソッドを直接呼び出して検証する
- OpenAPI生成済みrequest objectからusecase / service入力への変換を検証する
- usecase / serviceの結果からOpenAPI生成済みresponse objectへの変換を検証する
- usecase / serviceのエラーからresponse objectへの変換を検証する
- domainロジックは一切テストしない
- OpenAPI validator、generated router、JSON decode、path parameter bindなどのHTTPリクエスト検証はintegration testで扱う

例:

- `src/internal/presentation/handler/..._handler_test.go`

## 6. テスト責務の境界

### domain
- 純粋ロジックの検証

### usecase
- 業務フローの検証(repositoryはmock)

### handler
- request object / response object 変換の検証(usecase / serviceはmock)

### integration
- HTTPリクエスト、OpenAPI validator、generated router、handler、usecase / service、repository、DBを含む結合検証(外部APIはmock)

## 7. 判断ルール

迷った場合は以下を優先する:

- unit testを優先する
- 外部依存を増やさない
- mock化できるものはすべてmock化する

## 8. e2eの扱い(重要)

e2eテストは「必須ではない」。

以下の場合のみ追加する:

- 金銭系(決済など)
- 重要な業務フロー
- システム全体の整合性確認が必要な場合

それ以外はintegration testで十分とする

 

・「src/AGENTS.md」(Goの実装ルール定義)

# Goの実装ルール定義

このファイルをGoの実装における「実行ルールの入口」として扱う。 
詳細な設計ルールはすべて `docs/rules` 配下を参照すること。

Go実装時は、まずこのファイルを読み、実装プロセスを確認すること。
そのうえで、判断に必要な詳細ルールを以下から参照する。

1. `docs/rules/dependency-rules.md`
2. `docs/rules/architecture.md`
3. `docs/rules/module-classification.md`
4. `docs/rules/oapi-codegen.md`
5. `docs/rules/orm-bun.md`
6. `docs/rules/testing.md`

矛盾が発生した場合は、リポジトリ直下の `AGENTS.md` の「ルール優先順位」に従うこと。

## 1. 基本原則

- 必ずTDD(テスト駆動開発)で実装する
- Red → Green → Refactor を厳守する
- 既存構造を勝手に変更しない
- ルールに存在しない構造は作らない
- すべての判断は `docs/rules` を最優先とする
- 推測で設計判断を行わない

## 2. 実装プロセス(必須手順)

新規APIの作成・既存APIの変更は必ず以下の順序で行う。
API仕様の確定とコード生成はRedテスト作成より前に完了させる。

### 1. モジュール分類

まず `docs/rules/module-classification.md` を参照し、対象機能を分類する。

分類先:

- core
- supporting
- generic
- shared

配置先は分類結果に従うこと。

---

### 2. OpenAPI定義を修正する

APIの追加・変更を行う場合は、実装より先にOpenAPI定義を更新する。

API仕様を先に確定させること。

対象:

- `src/openapi/openapi.yaml`
- `src/openapi/paths/*`
- `src/openapi/components/schemas/*`

---

### 3. コード生成を実行する

OpenAPI定義を更新した場合は、必ず以下のコマンドを実行し、コード生成を行う。

```bash
make generate
```

生成コードは以下に出力される。

- `src/internal/presentation/gen`

生成コードは手動編集してはならない。

禁止事項:

- `make generate` 実行前に生成コードを編集してはならない
- `src/internal/presentation/gen` 配下を手動編集してはならない

---

### 4. テストコードを先に作成する(Red)

実装前に必ず失敗するテストを書く。

テストルールは docs/rules/testing.md に従うこと。

テスト追加後は、対象テストを実行し、期待どおり失敗することを確認する。

実行コマンド:

・ユニットを追加した場合

```bash
make test-unit
```

・インテグレーションテストを追加した場合

```bash
make test-integration
```

・e2eテストを追加した場合

```bash
make test-e2e
```

失敗を確認せずに実装を開始してはならない。

---

### 5. 実装する(Green)

テストを通すための最小実装を行う。

以下のルールを厳守すること。

- `docs/rules/architecture.md`
- `docs/rules/dependency-rules.md`
- `docs/rules/module-classification.md`
- `docs/rules/oapi-codegen.md`
- `docs/rules/orm-bun.md`

実装後は、追加・変更したすべてのテストが成功することを確認する。

実行コマンド:

```bash
make test
```

個別実行する場合:

・ユニットテスト実行

```bash
make test-unit
```

・インテグレーションテスト実行

```bash
make test-integration
```

・e2eテスト実行(存在する場合のみ)

```bash
make test-e2e
```

Green未達の状態でリファクタリングを行ってはならない。

---

### 6. リファクタリングする(Refactor)

テスト成功後にのみリファクタリングを行う。

- 振る舞いを変更しない
- テストを壊さない
- 構造のみ改善する

---

### 7. フォーマット・静的解析を実行する

コードの追加・修正後は、必ずフォーマットおよび静的コード解析を実行する。

実装完了条件は、以下のコマンドがすべて成功すること。

・フォーマット修正

```bash
docker compose run --rm api golangci-lint fmt -v ./...
```

・静的コード解析

```bash
docker compose run --rm api golangci-lint run -v ./...
```

違反が検出された場合は、すべて解消してからレビュー依頼を行うこと。

生成コードを含め、リポジトリ全体がチェック対象となる。

---

## 3. 判断ルール

判断に迷った場合は以下を参照する。

- `docs/rules/dependency-rules.md`
- `docs/rules/architecture.md`
- `docs/rules/module-classification.md`
- 既存コード

それでも判断できない場合は、`docs/rules/module-classification.md` の分類基準を再確認すること。

推測による配置判断・設計判断は禁止する。

## 4. 禁止事項

以下を禁止する。

- OpenAPI定義を変更せずにAPIを追加・変更すること
- OpenAPI定義より先にAPI実装を開始すること
- make generate を実行せずに生成コードを利用すること
- 生成コードを手動編集すること
- docs/rules に存在しない構造を追加すること
- 推測でモジュール分類を行うこと
- 推測で依存関係を追加すること
- テストを書かずに実装を開始すること
- Redを経ずにGreenを行うこと
- Redを確認せずに実装を開始すること
- Greenを確認せずにリファクタリングを行うこと
- フォーマットを実行せずにレビュー依頼すること
- 静的コード解析を実行せずにレビュー依頼すること
- テストが失敗した状態でレビュー依頼すること
- golangci-lint run でエラーが残った状態でコミットすること
- テストが失敗した状態でコミットすること

 

・「AGENTS.md」(全体のルール定義)

# 全体のルール定義

## 概要

本プロジェクトはマルチエージェント構成(Codex Agents)を前提とする。

以下のエージェントが `.codex/agents` に定義されており、
すべての開発は責務分離されたTDDフローに従って実行される。

本プロジェクトは以下を前提とする:

- OpenAPI駆動開発
- TDD(Red → Green → Refactor)
- 制約ベースアーキテクチャ(DDD簡略モデル)

## エージェント構成

### pm

- 要件整理・仕様定義
- ユーザーストーリー作成
- タスク分解・優先順位決定
- 実装詳細には踏み込まない

### tester

- テスト設計(REDフェーズ)
- 失敗するテストコード作成
- 仕様の穴の検出
- API期待仕様の明文化

### implementer

- 実装(GREENフェーズ)
- テストを通す最小実装
- 設計判断は禁止(ルール準拠のみ)
- OpenAPI生成コードを前提に実装

### reviewer

- コードレビュー
- 依存ルール違反の検出
- アーキテクチャ逸脱の検出
- テストの妥当性検証
- 実装修正は行わない(再実装指示のみ)

## ワークフロー

開発は必ず以下のTDDフローに従う:

- `.codex/workflows/tdd_flow.md`

## ルール参照構造

エージェントは実装判断前に以下を参照する:

### アーキテクチャ

- `docs/rules/architecture.md`

### モジュール配置

- `docs/rules/module-classification.md`

### 依存関係

- `docs/rules/dependency-rules.md`

### OpenAPI生成ルール

- `docs/rules/oapi-codegen.md`

### ORM(Bun)

- `docs/rules/orm-bun.md`

### テスト

- `docs/rules/testing.md`

## 実装ルール

実装詳細はすべて以下に委譲する:

- `src/AGENTS.md`

※ implementer は必ずこれに従う

## ブランチ運用ルール

すべての実装・修正・テスト作成は、必ずブランチを切ってから開始すること。

### ブランチ命名規則

```
<prefix>/<short-description>
```

例:

- feat/user-registration
- fix/user-login-error

---

### ブランチプレフィックス定義

種別の分類:

- feat: ユーザーに価値を提供する新機能
- fix: 不具合の修正
- refactor: 挙動を変えない内部改善
- perf: 性能改善を主目的とした修正
- docs: ドキュメントの追加・更新
- test: テストの追加・修正
- infra: インフラ・CI/CD・環境構築
- chore: 上記に当てはまらない雑務(極力使わない)

---

### ブランチ作成ルール(必須)

- テスト作成前に必ずブランチを作成する
- 実装開始前に必ずブランチを作成する
- 1タスク = 1ブランチを原則とする
- ブランチは短命に保つ

---

### TDDとの関係

- REDフェーズ開始前にブランチを作成する
- GREEN実装は必ず当該ブランチ内で行う
- reviewerのNGによる再実装も同一ブランチで継続する

---

## ルール優先順位

矛盾が発生した場合は以下の優先順位で解決する:

1. dependency-rules.md
2. architecture.md
3. module-classification.md
4. src/AGENTS.md
5. oapi-codegen.md
6. orm-bun.md
7. testing.md

## エージェント責務原則

### 共通原則

- 各エージェントは責務外の判断を行わない
- 設計と実装は必ず分離する
- TDDフローをスキップしない
- 推測による実装禁止

## 責務分離

### pm

- 仕様決定・タスク分解
- 実装禁止

### tester

- テストのみ(RED)
- 実装禁止

### implementer

- 実装のみ(GREEN)
- 設計判断禁止
- ルール準拠のみ

### reviewer

- 検証のみ
- 実装修正禁止
- NG時は再実装指示のみ

## TDD制約(重要)

- REDなしで実装開始禁止
- REDレビューなしでGREEN禁止
- GREEN未達でレビュー禁止
- GREENレビューなしで完了禁止
- reviewer NG時は必ず再実装ループ

## 禁止事項

- 本ファイルに実装ルールを書くこと
- エージェントの責務を曖昧にすること
- TDDフローを省略すること
- reviewerが実装修正を行うこと
- 存在しないルールファイル(旧設計含む)を参照対象にしないこと

 

ハーネス設計の注意点

上記では様々なドキュメントを定義してハーネス設計を行なっていますが、それぞれのドキュメントは完璧なものではありません。

もし実務でハーネス設計をする必要がある場合は、それぞれのプロジェクトに応じて最適なハーネス設計をするようにして下さい。

また、これらのドキュメントは一回作って終わりではなく、適宜改善して育てていく必要性もあると思うので、その点も注意しましょう。

 

スポンサーリンク

Lefthook・Git・GitHubを利用して管理する

Gitフック管理ツール「Lefthook」を導入(任意)

次にチーム開発向け(任意)になりますが、Go言語製のGitフック管理ツール「Lefthook」を導入し、フォーマッターや静的コード解析、そしてテスト実行を自動化させます。

ツールはHomebrewで入れるため、以下のコマンドを実行してインストールして下さい。

$ brew install lefthook

 

次に以下のコマンドを実行し、lefthookを導入します。

$ lefthook install

 

次に作成されたファイル「lefthook.yml」を以下のように修正します。

pre-commit:
  parallel: true
  commands:
    format:
      run: docker compose run --rm api golangci-lint fmt -v ./...
    lint:
      run: docker compose run --rm api golangci-lint run -v ./...

pre-push:
  commands:
    test:
      run: |
        docker compose up -d
        make test

※pre-commit設定にコミット時に実行させたい処理、pre-push設定にプッシュ時に実行させたい処理を設定します。

 

関連記事

Go言語(Golang)開発でLefthookの使い方|pre-commitでformat・lint(静的解析)を自動化
こんにちは。Tomoyuki(@tomoyuki65)です。フロントエンドのチーム開発では、Node.js環境で利用できるHuskyといったライブラリを用いて、Gitのフックを使ったformat(コード整形)やlint(静的解析)の自動化を...

 

GitHub ActionsによるCIの導入

次に今回はコード管理にGitHubを利用するため、GitHub ActionsによるCI(Continuous Integration:継続的インテグレーション)も導入しておきます。

まずは以下のコマンドを実行し、設定用のファイルを作成します。

$ mkdir -p .github/workflows && touch .github/workflows/ci.yml

 

次に作成したファイルを以下のように記述します。

name: CI

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  # 変更ファイル判定(全体トリガー)
  changes:
    runs-on: ubuntu-latest

  outputs:
    app: ${{ steps.filter.outputs.app }}

  steps:
    - name: リポジトリをチェックアウト
      uses: actions/checkout@v6

    - name: 差分チェック
      uses: dorny/paths-filter@v4
      id: filter
      with:
        filters: |
          app:
            - 'src/**/*.go'
            - 'src/go.mod'
            - 'src/go.sum'

# 静的コード解析
lint:
  needs: changes
  if: needs.changes.outputs.app == 'true'
  runs-on: ubuntu-latest

  steps:
    - name: リポジトリをチェックアウト
      uses: actions/checkout@v6

    - name: 環境変数ファイルをリネーム
      run: cp ./.env.example ./.env

    - name: Dockerコンテナのビルド
      run: docker compose build

    # lintの実行
    - name: lint
      run: docker compose run --rm api golangci-lint run -v ./...

# テスト
test:
  needs: changes
  if: needs.changes.outputs.app == 'true'
  runs-on: ubuntu-latest

  steps:
    - name: リポジトリをチェックアウト
      uses: actions/checkout@v6

    - name: 環境変数ファイルをリネーム
      run: cp ./.env.example ./.env

    - name: Dockerコンテナのビルド
      run: docker compose build

    - name: Dockerコンテナの起動
      run: docker compose up -d

    - name: DB接続待機処理
      run: docker compose exec app bash -c "until pg_isready -h db -U ${DB_USER}; do sleep 1; done"

    - name: マイグレーションの初期化
      run: docker compose exec api env ENV=testing go run cmd/migrate/main.go init

    - name: マイグレーションの実行
      run: |
        if ls src/internal/infrastructure/migrations/*.sql >/dev/null 2>&1; then
          docker compose exec api env ENV=testing go run cmd/migrate/main.go migrate
        else
          echo "No migrations found. Skip."
        fi

    - name: テストの実行
      run: make test

※起動条件はプルリクエスト(PR)作成時か、mainブランチへのプッシュやマージ実行時です。加えて対象ファイルの差分変更を検知して各種処理を実行させてます。「actions/checkout@v6」や「dorny/paths-filter@v4」はnodeのバージョンが上がった際にバージョンを上げる必要がでる可能性があります。尚、将来的にテストが多くなって完了時間が長くなった場合は、テストの並列実行などの検討が必要になったりします。

 

Git管理

次にここまでのコードをGit管理できるようにするため、以下のコマンドを実行します。

$ git init
$ git add -A
$ git commit -m "init"

 

GitHubのリポジトリに登録

次に上記でGit管理できるようにしたコードをGitHubのリポジトリに登録します。

登録方法の詳細は割愛しますが、登録後に以下のような画面になります。

CIも実行されるので、CIの詳細を確認するにはメニュー「Actions」をクリックします。

 

次にActions画面で実行したワークフロー一覧が表示されるので、対象のワークフローをクリックします。

 

これで対象ワークフローの詳細を確認でき、全て緑色のチェックが付いて正常終了すればOKです。(lintやtestの部分をクリックして処理の詳細も確認できます。)

※今回は全て完了するのに1分33秒かかりました。

 

mainブランチなどの保護設定をしたい場合

実務などではmainブランチの保護設定が必要になったりしますが、その場合はリポジトリのメニューから「Settings」画面を開き、画面左のメニュー「Branches」を選択後、Branch protection rules画面が開くので、「Add branch ruleset」をクリックします。

 

次にRuleset Nameを入力後、Enforcement statusを「Active」に変更し、Target branchesの項目にある「Add target」から「Include by pattern」を選択します。

 

次にポップアップが表示されるので、「main」を入力して「Add Inclusion pattern」をクリックします。

 

これでTarget branchesの項目に対象のブランチ名「main」が設定されます。

 

次にマージする前にプルリクエストを必須にしたい場合は、「Require a pull request before merging」のチェックを付け、必要に応じてオプションを設定して下さい。

 

次にCIをPASS(正常終了)することを必須にしたい場合は、「Require status checks to pass」のチェックを付け、必要に応じてオプションを設定し、「Add checks」のリストからCIの対象のジョブを検索してチェックを付けて下さい。

 

対象のジョブを選択後、下図のように対象のジョブが表示されればOKです。

 

全ての設定完了後、画面下の「Create」をクリックします。

 

次に認証を求められるので、認証をして下さい。

※私はパスキー認証にしています。

 

これでブランチ保護設定が完了です。今回は一例ですが、他にも色々条件が付けられるので、必要に応じて任意のものを設定して下さい。

 

ラベル追加(任意)

リポジトリのIssues機能などで使うラベルは追加可能なので、今回はAI駆動開発用のラベルとして「AIDD」も追加しておきます。

 

スポンサーリンク

OpenAI「Codexアプリ」のプランモードで開発計画を立てる

次にAIツールであるOpenAIの「Codexアプリ」を使い、プランモードを用いて簡単な機能を追加する開発計画を立てます。

ただし、今回はAIツールとのやりとり回数を少なくするため、事前にある程度考えた機能の仕様を投げる形で進めます。

 

関連記事

OpenAI Codex CLI / Codexアプリの使い方【ChatGPT時代のAI開発ツール入門】
こんにちは。Tomoyuki(@tomoyuki65)です。2026年現在、AI戦国時代に突入し、めちゃめちゃ競争が激しいところですが、ついにOpenAI も新機能としてCodexアプリをリリースしたり、各種キャンペーンを展開したりしていま...

 

では今回は「会員ランクと購入金額に応じて、付与ポイントを計算して返す」ようなAPIをDDDで作ってみますが、事前に考えた仕様については以下の通りです。

▫️会員テーブル定義
・テーブル名
members

・テーブル定義
| カラム名 | データ型 | NULL許可 | デフォルト値 | 説明 |
| --- | --- | --- | --- | --- |
| id | UUID | × | - | 会員ID(主キー) |
| name | TEXT | × | - | 会員名 |
| rank | TEXT | × | - | 会員ランク |
| created_at | TIMESTAMPTZ | × | NOW() | 作成日時 |

・開発環境用マスタデータの例
| id | name | rank |
| --- | --- | --- |
| 11111111-1111-1111-1111-111111111111 | 田中 | bronze |
| 22222222-2222-2222-2222-222222222222 | 佐藤 | silver |
| 33333333-3333-3333-3333-333333333333 | 鈴木 | gold |

マスタデータ登録用のスクリプトは「src/cmd/seed/main.go」として作り、
マスタデータ登録用のseed定義は「src/internal/infrastructure/database/seed/seed.go」と、「src/internal/infrastructure/database/seed/local」配下にファイルを作って下さい。

▫️API定義

・エンドポイント
POST /members/{memberId}/point-calculations

・リクエストヘッダー
Content-Type: application/json

・パスパラメータ定義
| 項目名 | 型 | 必須 | バリデーション | 説明 | 例 |
| --- | --- | --- | --- | --- | --- |
| memberId | string | ○ | UUID形式 | 会員ID | 11111111-1111-1111-1111-111111111111 |

・リクエストボディ定義
| 項目名 | 型 | 必須 | バリデーション | 説明 | 例 |
| --- | --- | --- | --- | --- | --- |
| purchaseAmount | integer | ○ | 0以上999,999,999以下 | 購入金額(税込) | 5000 |

・リクエストボディの例
{
"purchaseAmount": 5000
}

・レスポンス定義
| 項目名 | 型 | 説明 | 例 |
| --- | --- | --- | --- |
| memberId | string | 会員ID | 11111111-1111-1111-1111-111111111111 |
| purchaseAmount | integer | 計算対象の購入金額 | 5000 |
| grantedPoint | integer | 付与予定ポイント合計 | 50 |

・レスポンスの例
{
"memberId": "11111111-1111-1111-1111-111111111111",
"purchaseAmount": 5000,
"grantedPoint": 50
}

・ステータスコード一覧
| ステータスコード | 説明 |
| --- | --- |
| 200 OK | ポイント計算成功 |
| 400 Bad Request | リクエスト不正 |
| 404 Not Found | 指定した会員が存在しない |
| 500 Internal Server Error | 予期せぬエラー |

・エラーのレスポンス定義
| 項目名 | 型 | 説明 | 例 |
| --- | --- | --- | --- |
| code | string | エラーコード | MEMBER_NOT_FOUND |
| message | string | エラーメッセージ | member not found |

・エラーのレスポンス例
{
"code": "MEMBER_NOT_FOUND",
"message": "member not found"
}

▫️付与ポイント計算ルール

・ポイント付与率
| ランク | ポイント付与率 |
| --- | ---: |
| bronze | 1% |
| silver | 3% |
| gold | 5% |


・倍率ルール
購入金額が10,000円以上の場合、計算後のポイントを2倍にする

・端数処理
小数点以下は切り捨てる

・計算手順
1. 対象の会員の会員ランクからポイント付与率を決定する
2. 購入金額とポイント付与率から付与ポイントを計算する
3. 購入金額が10,000円以上なら付与ポイントを2倍にする
4. 計算結果の付与ポイントの合計値について、小数点以下を切り捨てる

・計算例
| ランク | 購入金額 | 計算式 | 付与ポイント |
| --- | ---: | --- | ---: |
| bronze | 5,000 | 5,000 × 1% | 50 |
| silver | 5,000 | 5,000 × 3%` | 150 |
| gold | 15,000 | (15,000 × 5%) × 2 | 1,500 |

 

次にこの仕様を用いて「Codexアプリ」のプランモードで開発計画を立てます。

まずはアプリ起動後、画面左のメニューにあるプロジェクトの右側にあるボタンメニューから「既存のフォルダーを使用」をクリックします。

 

次にプロジェクト選択画面が表示されるので、対象のプロジェクトを選択後、「開く」をクリックします。

 

これで画面左のメニューにプロジェクトが追加されます。

 

次にチャット欄の左下にある「+」からメニューにあるプランモードをONにします。

※チャット欄の下のカスタムの右に「プラン」があるとプランモードの状態です。

 

次にチャット欄に命令内容を入力し、チャット欄の右下にある「↑」をクリックして実行します。

 

実行後、計画されたプラン内容が表示されるので、「プランを展開する」をクリックして内容を確認して下さい。

そして画面下でプランを実行するか聞かれるので、「閉じる」をクリックして一旦止めます。

 

次に計画の修正が必要な場合は、再度チャット欄に命令内容を入力し、チャット欄の右下にある「↑」をクリックして実行します。

 

実行後、修正されたプラン内容が表示されるので、「プランを展開する」をクリックして内容を確認して下さい。

そして画面下でプランを実行するか聞かれるので、「閉じる」をクリックして一旦止めます。

 

次に上記で作成したスキル「plan-to-issue」を使って計画したプランをGitHubのIssueに登録しますが、デフォルト設定ではネットワークアクセスを許可がOFFになっていて、スキルに記載したコマンドが実行できません。

そのため、Codexアプリの設定画面から構成を開き、ネットワークアクセスを許可するを一時的に許可します。

 

次にチャット欄のプランモードを解除し、「スキル「plan-to-issue」を使って、プランをGitHub Issueに登録して下さい。」を入力して実行します。

 

実行途中に3つのIssueに分けて登録するような感じになってしまったため、追加で「登録するIssueは一つにして」の指示をしています。

 

スキルの実行が上手く行くと、以下のように登録されたIssueのリンクが表示されるのでクリックします。

 

GitHubのIssueにプラン内容が登録されるので、内容を確認して下さい。

 

このようにAIツールのプランモードを活用(ハーネス設計を含む)すると、AIに実行させるためのタスクを作ることができ、このタスクを作る部分がAI駆動開発時代におけるエンジニアのメイン作業になります。

このプラン内容がAIがタスク実行した際のアウトプットの質に直結するため、プランモードで立てた計画内容や、別途Issueに登録するならIssueの内容はしっかりレビューし、タスク内容に過不足が無いかをしっかり確認して下さい。

 

スポンサーリンク

OpenAI「Codexアプリ」でタスク実行する

次に上記で作成したタスクを実行しますが、今回はまっさらな状態からGtiHubに登録したIssueの情報を取得して実行させるため、まずは画面左のプロジェクト名の右側のボタンをクリックし、先ほどとは別の新しいチャットを作ります。

 

次にGtiHubに登録したIssueの情報を取得するため、以下の命令を入力して実行します。

コマンド「gh issue view 1 --json title,body」を実行し、タスク内容を取得して下さい。

※コマンド「gh issue view {対象のIssue番号} –json title,body」

 

実行後、GtiHubに登録したIssueの情報を取得できればOKです。

 

次に以下の命令を入力して実行し、タスクの途中まで(テストコードをRedで作成)実行します。

AGENTS.mdを起点として4人のマルチエージェントでタスクを実行し、テストコードを作成するところまで進めて下さい。
完了後にレビューをしたいのでスキル「auto-commit」を使ってコミットし、その後にスキル「tdd-draft-pr」を使ってドラフトPRを作成して下さい。

※マルチエージェントを利用したい場合は、明示的に命令する必要があります。またマルチエージェントを利用するとトークン消費が激しくなるため、その点は理解して利用して下さい。

 

実行後、途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

※上記で設定でネットワーク許可をONにした際にファイル「.codex/config.toml」が更新されてしまいますが、これはコミットしたくないので対象から外します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

全て完了後、ドラフトPRのリンクが出力されるのでクリックします。

※タスク実行部分としては、約15分かかりました。

 

以下のようにドラフトPRが作成されていればOKです。内容を確認してレビューして下さい。

 

次にレビューで指摘がある場合、修正指示を出して改善を繰り返して下さい。

更新内容をドラフトPRに反映したい場合は、スキル「auto-commit」と「pr-sync-comment」を使ってドラフトPRに反映させる旨の命令をして下さい。

尚、今回はハーネス設計の作りが甘かったこともあり、5回ほどレビューと修正を繰り返し、レビュー対応では約3時間ほどかかりました。

 

次に以下の命令を入力して実行し、テストコードを通すように実装をさせます。

AGENTS.mdを起点として4人のマルチエージェントでタスクを実行し、テストコードをパスするように実装を進めて下さい。
完了後にスキル「auto-commit」と「tdd-ready-pr」を使ってドラフトPRを通常のPRに更新して下さい。

※マルチエージェントを利用したい場合は、明示的に命令する必要があります。またマルチエージェントを利用するとトークン消費が激しくなるため、その点は理解して利用して下さい。

 

実行後、途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

再度処理の途中で承認が求められるので、「はい」を実行します。

 

全て完了後、PRのリンクが出力されるのでクリックします。

※タスク実行部分としては、約8分かかりました。

 

以下のようにドラフトPRから通常のPRに更新されればOKです。内容を確認してレビューして下さい。

 

次にレビューで指摘がある場合、修正指示を出して改善を繰り返して下さい。

更新内容をPRに反映したい場合は、スキル「auto-commit」と「pr-sync-comment」を使ってPRに反映させる旨の命令をして下さい。

尚、今回はDDDとしてファイルを作るのが初回だったことや、テストコード作成時と同様にハーネス設計の作りも甘かったため、複数回のレビューと修正を繰り返し、レビュー対応では約3時間ほどかかりました。

 

スポンサーリンク

AI駆動開発で作ったAPIの検証

次にAI駆動開発で作ったAPIをローカル環境で検証してみます。

まずは以下のコマンドを実行し、Dockerコンテナの再起動とDB設定を行います。

$ docker compose down -v
$ docker compose build --no-cache
$ docker compose up -d
$ docker compose exec api go run cmd/migrate/main.go init
$ docker compose exec api go run cmd/migrate/main.go migrate
$ docker compose exec api go run cmd/seed/main.go

 

次にPostmanを使ってAPIを実行してみます。

POSTメソッドでURL「http://localhost:8080/api/v1/members/{{memberId}}/point-calculations」を入力、memberIdに「33333333-3333-3333-3333-333333333333」を入力、ボディに「{ “purchaseAmount”: 1000 }」を入力後、「送信」ボタンをクリックします。

実行後、想定通りの結果で正常終了すればOKです。

 

 

す、凄いぞ!!

 

 

AI駆動開発における開発効率について

今回のタスク内容について、もしAIを使わずにやった場合、私の想定だと最低でも2営業日ぐらいはかかる想定をしていましたが、コードを書く部分だけなら合計で約23分ほどで完了しました。

また、今回はハーネス設計が甘かったり、DDDとして初回の作成だったこともあり、レビュー時間が合計で約6時間ほどかかってしまったため、レビュー込みだと合計で約383分ほどでの完了となりました。

そのため、ハーネス設計を改善すれば、次回以降はレビュー時間も2時間ぐらいに短縮できそうではあるため、そうなるとレビュー込みで合計で約143分ぐらいで完了できそうな感じです。

それらを踏まえて開発効率を表にまとめると以下のようになります。

項目 想定(AIなし) 今回実績(AIあり) 改善後見込み
実装時間 約960分(2営業日) 約23分 約23分
レビュー時間 約360分(6時間) 約120分(2時間)
合計時間 約960分 約383分 約143分
開発効率 1.0倍 約2.5倍 約6.7倍
工数削減率 約60.1% 約85.1%

 

このようにAI駆動開発をすることで、DDDのような複雑性が高いAPI開発でも、約3〜7倍ほどの開発効率アップが見込めます!

 

スポンサーリンク

レビュー完了後にブランチのマージとIssueのクローズ

レビューも完了し、全て終わった場合、PRのコメントに「LGTM!」を記載したりします。

※LGTMは、「Looks Good To Me」の略で、コードレビューなどで「私的に良さそう(修正不要で承認)」を意味するフレーズです。チャットツールなどで承認の意思を素早く伝える際によく利用されます。

 

PR完了後にブランチをマージしたい場合は、PRの下にある「Merge pull request」をクリックします。

※今回はサクッとマージしますが、PR毎にどのブランチにマージされるかが設定されている(一番上の部分に表示されています。変更も可能です。)ので、そこが間違えてないかは気にするようにして下さい。

 

次に「Confirm merge」をクリックします。

 

これでマージ完了です。(今回の例ではmainブランチにマージされてます。)

また、今回はブランチを残していますが、マージ後にPull request successfully merged and closedの右側にブランチ削除ボタン「Delete branch」が表示されるので、ここをクリックしてブランチ削除が可能です。

実務であれば無駄なブランチが溜まっていくので、マージ済みのブランチは削除していく方がいいかなと思います。

 

次に対象のIssueがあれば確認し、必要に応じてクローズする旨のコメントを記述し、「Close with comment」をクリックします。

 

これでIssueのクローズも完了です。

 

スポンサーリンク

本番環境用のDockerコンテナについて

上記でローカル開発環境でAPIを作りましたが、これを本番環境にデプロイする際は専用のDockefileを作って一つのDockerコンテナで起動させれるようにする必要があります。

次に以下のコマンドを実行し、各種ファイルをを作成します。

$ mkdir -p deploy/docker/prod && touch deploy/docker/prod/Dockerfile
$ touch .env.production

 

次に作成したファイルをそれぞれ以下のように記述します。

・「deploy/docker/prod/Dockerfile」

####################
# ビルドステージ
####################
FROM golang:1.26.3-alpine3.23 AS builder

WORKDIR /go/src

COPY ./src .

# 依存関係をインストール
RUN go install

# ビルド(GOOS:OS指定、GOARCH:CPUアーキテクチャ指定)
RUN GOOS=linux GOARCH=amd64 go build -o main .

####################
# 実行ステージ
####################
FROM alpine:3.23 AS runner

# タイムゾーンを設定
ENV TZ=Asia/Tokyo

# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(--no-cacheでキャッシュ削除)
    apk add --no-cache \
            tzdata

WORKDIR /app

# コンテナ用ユーザー作成
RUN addgroup --system --gid 1001 appuser && \
    adduser --system --uid 1001 appuser

# ビルドステージで作成したバイナリをコピー
COPY --from=builder --chown=appuser:appuser ./go/src/main .

# ポートを設定
EXPOSE 8080

# コンテナ起動ユーザー設定
USER appuser

# APIサーバー起動コマンド
CMD ["./main"]

 

・「.env.production」

ENV=local
PORT=8080
REQUEST_TIMEOUT_SECONDS=10
CORS_ALLOWED_ORIGIN=*
OTEL_EXPORTER_TYPE=none
DB_NAME=local-db
DB_USER=root
DB_PASSWORD=root-pass
DB_HOST=host.docker.internal
DB_PORT=5432
DB_MAX_OPEN_CONNS=2
DB_MAX_IDLE_CONNS=2
DB_CONN_MAX_LIFETIME=5

※ENVの値は本来なら「production」を設定しますが、今回はローカルのDBに接続させて確認するので「local」を設定します。また、今回はローカルのDBと接続して試すため、DB_HOSTは「host.docker.internal」を設定します。尚、ここでは.env.productionを使ってDockerコンテナに環境変数を渡していますが、実際の本番環境では.envは使わずに各インフラにあるシークレットサービスを利用して下さい。

 

次に以下のコマンドを実行し、起動中のDockerコンテナを止めます。

$ docker compose down

 

次に以下のコマンドを実行し、DBコンテナのみ起動します。

$ docker compose up -d db

 

次に以下のコマンドを実行し、上記で作成したDockerfileでDockerコンテナをビルドします。

$ docker build --no-cache -t go-oapi-aidd:1.0.0 -f deploy/docker/prod/Dockerfile .

※本番環境用を想定し、タグにはバージョン「1.0.0」を付けてます。

 

次に以下のコマンドを実行し、Dockerコンテナを起動します。

$ docker run -d \
-p 80:8080 \
--env-file .env.production
go-oapi-aidd:1.0.0

 

コマンド実行後、Docker Desktopを確認し、以下のように二つのDockerコンテナが起動できていればOKです。

※尚、dockerコマンドで起動させたDockerコンテナを止めたい場合は、Docker Desktopからだとゴミ箱アイコンをクリックして簡単に削除できます。

 

次にPostmanを使ってAPIを実行してみます。

POSTメソッドでURL「http://localhost/api/v1/members/{{memberId}}/point-calculations」を入力、memberIdに「33333333-3333-3333-3333-333333333333」を入力、ボディに「{ “purchaseAmount”: 1000 }」を入力後、「送信」ボタンをクリックします。

実行後、想定通りの結果で正常終了すればOKです。

 

スポンサーリンク

AIツール利用時のセキュリティ対策について

AIツールは便利ですが、セキュリティ知識が無い状態で安易に利用すると、知らないうちに意図せず機密情報を漏洩し、セキュリティ問題に発展する可能性があります。

最低限覚えておきたいセキュリティ知識については以下の記事にまとめたので、合わせでご確認下さい。

 

関連記事

生成AIで機密情報は大丈夫?AIツール開発のセキュリティ対策|ゼロトラスト・サンドボックス・環境変数(.env)管理
こんにちは。Tomoyuki(@tomoyuki65)です。近年は生成AIが急激に普及し、非エンジニアの方も含めて、様々な方が各種AIツールを利用し始めていると思います。ただその一方で、"機密情報の漏洩"といったセキュリティ問題も多発してお...

 

スポンサーリンク

最後に

今回はGo言語(Golang)とAI駆動開発で実践するDDDベースのAPI開発方法についてご紹介しました。

今回はこれまでの経験を踏まえて、Go言語でAI駆動開発を試しましたが、DDDのような複雑性が高いAPI開発でも、約3〜7倍ほどの開発効率アップを実現できました!

また、今回改めて理解が深まったこととしては、レビュー工数削減のためにはしっかりハーネス設計する必要があるため、実務ではやはりハーネスエンジニアリングが大事になりそうです。

そんな感じで、2026年以降はAI駆動開発が必須になっていくため、Go言語のAPI開発でAI駆動開発をしていきたい方は、ぜひ参考にしてみて下さい!

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はSoftware Engineer(Web/Gopher)をしています!

Tomoyukiをフォローする
3. 応用
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました