Technote

by sizuhiko

CodeCommit と CodeBuild を GitHub と Actions の組み合わせのように使う

この記事は AWS の CodeCommit と CodeBuild を使って「 GitHub と Actions だったら簡単にできる PR のビルドが成功したときだけマージ可能になる」仕組みを作ってみたので、その手順をまとめました。

それって標準でできないの?

はい、 AWS の Code 兄弟だけでソースコードを管理している場合、CodeCommit には PR の機能と承認フローの制御はあるけど、ビルド結果と連動した仕組みは用意されていません。 僕も担当するプロジェクトで使うまで、それぐらいあるやろーと思っていたのですが、無いと聞いて衝撃を受けました。

前提

CodeCommit 上のリポジトリのブランチ戦略は git-flow で運用しています。 なので、 以下のようなブランチやタグが存在します。

  • master
  • develop
  • release/v9.9.9
  • hotfix/v9.9.9
  • feature/xxxx-xxx

CodeCommit と CodeBuild で git-flow を取り扱う

この時点でかなり面倒なのですが、以下のQiita記事が参考になります。

なんで StepFunctions を使うかというと、Code兄弟を組み合わせて使ったとき、 feature/* のような動的ブランチの PR に対してビルドを実行できないといった問題があるためです。 feature/* に PUSH されたときに CodeBuild が動くとかはできるんですけど、そういうことじゃないんだよ Code兄弟よ。わかってないな、というとこ。

EventBridge -> StepFunctions への連携

CodeCommit への PR や PUSH のイベントを EventBridge で受け取って StepFunctions へ受け流します。

こちらの記事が参考になります。

CodeCommit の PR や PUSH のイベントが発生したら、StepFunctions を呼び出すように設定します。

基本的には事前定義パターンでも問題ないと思いますが、EventBridgeの段階でイベントを絞り込みたい場合は、カスタムで絞り込んでも良いと思います。 対象イベントをどうするかは EventBridge でも StepFunctions で判定してもどちらでも良いでしょう。実行コストを機にするなら EventBridge でフィルタしたほうが良いですね。

たとえば PR の作成とソースの更新時にだけ動かしたい場合は、以下のようなJSONになります。

{
  "detail": {
    "event": ["pullRequestCreated", "pullRequestSourceBranchUpdated"]
  },
  "detail-type": ["CodeCommit Pull Request State Change"],
  "resources": ["CodeCommitのARN"],
  "source": ["aws:codecommit"]
}

GitHub Actions に置き換えると、以下のような部分を EventBridge や StepFunctions で設定すると思っていれば間違いないです。

on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - 'feature/**'
      - 'release/**'
      - develop
      - master
  push:
    branches:
      - 'release/**'
      - develop
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

StepFunctions のフロー

参考記事に StepFunctions から CodeBuild を実行する例が出ているので、そちらを参考にしてください。

フローとしては以下の様な流れを設定しています。

  1. PR かどうか判定する $.detail-typeChoise で判定して分岐
    1. PR だったら CodeBuild の build を実行して、 ResultPath: $.BuildResult に結果を格納
    2. ビルド結果を CodeCommit に通知する Lambda を実行
  2. 1 の ELSE
    1. PUSH になるので、ブランチ $.detail.sourceReferencerefs/heads/developrefs/heads/master , refs/heads/release にマッチしていたら CodeBuild の deploy を実行して、 ResultPath: $.BuildResult に結果を格納

実際はチャットにもビルド結果を通知したりしているので、フローはもう少し複雑ですが、大まかには上記のとおりです。

StepFunctions から CodeBuild を呼び出す時の設定可能なパラメータなど。AWSの公式ドキュメントを見るとわかりやすいです(翻訳のタイトル変だけどw)

Step AWS CodeBuild Functions による呼び出し

git-flow の CodeBuild プロジェクトを設定するときは、ソースの設定を指定せずに PR のソースブランチに対してビルドするようにします。

新機能 – Step Functions と AWS CodeBuild を使用した継続的インテグレーションワークフローの構築の記事で CodeBuild を実行するときの StepFunctions 定義は以下のようになっています。

    "Trigger CodeBuild Build With Tests": {
        "Type": "Task",
        "Resource": "arn:${AWS::Partition}:states:::codebuild:startBuild.sync",
        "Parameters": {
          "ProjectName": "${projectName}"
        },
        "Next": "Get Test Results"
    },

Parameters に以下のパラメータを追加して、特定のソースブランチに対してビルドできるように設定します。

  • SouceTypeOverride: “CODECOMMIT”
  • SourceLocationOverride: CodeCommit のURL
  • BuildspecOverride: 実行するビルドスペックのパス
  • SourceVersion: ブランチ名(event.detail.sourceReference)

CodeBuild でやること

CodeBuild では分岐処理を書くとしてもシェルの if 文を使うことになるので、そういったケース分けは StepFunctions で分岐して、buildspecfile を分けておいて BuildspecOverride で切り替える様にしておいた方が良いです。

GitHub Actions でいうと、単純なジョブが1つ書けるだけと思っていたほうが良いです。

CodeCommit にビルド結果を通知する

では CodeBuild の結果を CodeCommit に通知してみましょう。 ここは連携の仕組みはないので、 CodeCommit のコメント欄と承認機能を利用します。

まずビルド結果を CodeCommit にコメントして、ビルドが成功していたら承認する Lambda を実装します。

import { CodeCommitClient, PostCommentForPullRequestCommand, UpdatePullRequestApprovalStateCommand } from "@aws-sdk/client-codecommit":

export const handler = async (event) => {
    const client = new CodeCommitClient();
    const buildResult = event.BuildResult.Error ? JSON.parse(event.BuildResult.Cause) : event.BuildResult;
    const buildUrl = `https://リージョン.console.aws.amazon.com/codesuite/codebuild/コードビルドID/projects/${buildResult.Build.ProjectName}/build/${buildResult.Build.Id}`;
    const icon = event.BuildResult.Error ? '' : '';
    const content = `${icon} ビルド [${buildResult.Build.Id}](${buildUrl})`;
    const input = {
        pullRequestId: event.detail.pullRequestId,
        repositoryName: event.detail.repositoryNames[0],
        beforeCommitId: event.detail.sourceCommit,
        afterCommitId: event.detail.destinationCommit,
        content,
    };
    const command = new PostCommentForPullRequestCommandI(input);
    await client.send(command);

    if (!event.BuildResult || event.BuildResult.Error) {
        return;
    }
    const approvalInput = {
        pullRequestId: event.detail.pullRequestId,
        revisionId: event.detail.revisionId,
        approvalState: "APPROVE",
    };
    const approvalCommand = new UpdatePullRequestApprovalStateCommand(approvalInput);
    await client.send(approvalCommand);
}

本当は絵文字じゃなくて、バッジを使いたかったのですけど、CodeCommit では利用できる Markdown 記法に制限があって、外部画像は利用できないようです。 CodeBuild にもバッジ機能はありますが、 git-flow のように feature/* のワイルドカードブランチで、 StepFunctions からビルド対象ブランチを指定して実行する場合にはバッジが作れないので、それも使えません。 ここは絵文字一択でしょう。 あとは、ビルドが成功していても失敗していても、ビルド結果に飛べるリンクを付けることで確認しやすくなります。

この Lambda からは以下の2つのコマンドを利用するためのポリシーが必要になるので、忘れずにアタッチしましょう。

  • PostCommentForPullRequest
  • UpdatePullRequestApprovalState

ビルドが成功したときだけマージ可能にする

上記の Lambda でビルドに成功したときだけ承認を実行するようにしました。 CodeCommit では「承認ルールテンプレート」を設定できます。

以下のルールを前提とします。

  • CI でのビルドが成功している
  • PR 作成者以外の人が承認している

まず前者の承認ルールテンプレートを設定してみましょう。

  • 必要な承認の数: 1
  • 承認プールのメンバー
    • IAMユーザー名または引き受けたロール
    • 値: ビルドが成功していたら承認する Lambda のロール名
  • ブランチ名: develop

git-flow では feature/* から develop に向けて PR を出すので、ブランチフィルターを設定しておくと良いです。 また、Lambda からの承認であることを判定するには、Lambda に割り当てているロール名で判定するのが楽だと思うので、ロール名で判定すると良いでしょう。

続いて後者の承認ルールテンプレートを設定してみましょう。

  • 必要な承認の数: 2
  • ブランチ名: develop

ここでは承認者の絞り込みはしておらず、2つの承認があったらというルールにしています。 プロジェクトによっては特定の人の承認を必要とする場合もあるでしょうから、そのあ場合は承認プールのメンバーを設定して必要な承認数を指定してください。 ここでは誰かしら1人以上が承認してくれたら、という前提になっています。

ではなぜ必要な承認の数が 2 なのか?というと、CIでの承認数1と人の承認数1を合わせて2以上という形にしています。 人が2人以上承認していてもCIが失敗していた場合は、前者の承認ルールを満たしていないのでマージすることはできません。

ビルド成功で1、人の承認が1つ以上でルールが満たされます。

さいごに

これらの手順で CodeCommit / CodeBuild を使ったときに GitHub と Actions で簡単にできていたワークフローを実現できます。

面倒だ、面倒すぎる、と思いますよね。

できれば Code兄弟を避けていきたいのですが、プロジェクト事情でそれ以外の選択肢が選べないこともあるでしょう。 そのようなときに、この記事が参考になればと思います。 ここまで全体の手順を解説してくれる記事が見当たらなかったので、細かい設定は置いておいて大まかなら流れをベースに解説しました。 それぞれの部分(たとえばStepFunctionsとCodeBuildの連携)といった記事は検索すると見つかるので、具体的な実装方法はそれぞれの最新情報を確認ください。

Code兄弟さん、もっと便利になってくれないかな、と願う日々です。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 5

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 4 の続編となります。

GAEにデプロイして動作確認しながら修正

前回で Slim4 へ移行でき、少し動く様になってきたので動作確認しながら微修正していきます。

GAE 2nd gen 用の設定変更

app.yml ファイルに定義していたデプロイ対象外ファイルの一覧 skip_files は別のファイル .gcloudignore に記述するように変更になったので、定義を移行しました。そのコミット

Carbon のバージョンアップ

8.2 環境で動くように Carbon を 1.21 から 2.72 にアプデしました。そのコミット

composer ファイルをデプロイ対象に追加

GAE 2nd gen からは composer での依存関係はGAEデプロイ時に解決されるので、デプロイ対象ファイルに composer.json と composer.lock ファイルを追加しました。そのコミット

ミドルウェアの定義を変更

ミドルウェアがうまく動作していないことがわかったので、調べていると書き方が変わっていることに気づきました。すでにパラメータは変更していたのですが、実行メソッド名も変更になっていました。

そこでt PHP5.5 のときは interface を実装するコードになっていなかったので、ちゃんと MiddlewareInterface を実装するようなコードに修正。

use Psr\Http\Server\MiddlewareInterface;

class AllowedProvidersMiddleware
class AllowedProvidersMiddleware implements MiddlewareInterface
{

そうすると、以前は __invoke メソッドで定義していたところを process に変更する必要があることがわかりました。 その修正コミット

セッションの処理も修正

さきほどのコミットでは、セッション保持の Memcache がうまく動作していなかったので、いったん session_set_save_handler はコメントアウトして、デフォルトの tmp 管理にしています。ただ複数インスタンス起動するとうまく動作しなくなるので、いったん動作確認を進める上での暫定対応です。

またセッションミドルウェアも全体に対して有効にするのでなく、セッションが必要なAPI(ここでいうと管理画面のログイン周辺)についてだけ有効にするようにルーティングを変更しています。

その後、Google Cloud の Cloud Datastore に DatastoreSessionHandler というものがあるのがわかり、以下のように設定を変更しました。そのコミット

use Google\Cloud\Datastore\DatastoreClient;
use Google\Cloud\Datastore\DatastoreSessionHandler;

    $datastore = new DatastoreClient();
    $handler = new DatastoreSessionHandler($datastore);
    session_set_save_handler($handler, true);
    session_save_path('sessions');

POST のボディに JSON を渡す場合の対応

Slim4 では addBodyParsingMiddleware を利用する必要があったので、修正しました。そのコミット

併せて BASIC 認証時の before 処理も不要であることがわかったので削除しています。

GAE へのデプロイ方法を README に追記

GAE 2nd gen からはビルド結果を以下のコマンドでデプロイします。

gcloud app deploy --project toiletevolution --version 2 --no-promote --appyaml=app.yml

version を指定して新しいバージョンで動作確認を可能にして、 --no-promote をつけることで従来のURLからのアクセスは、新しいバージョンに振り分けられない様にします。

そしてどうなったか

この記事は PHP のバージョンアップをメインにしているのですが、実際には WebComponents 側も修正しています。

そしてシュミレータで一ヶ月ぐらいの稼働テストを実施して、問題なさそうだったので某日に正式リリースを実施しました。 その後、新しいバージョンでの問題も発生せず順調に動いています。

これでしばらくは落ち着くのと、バージョンアップ追従もどんどんできるようになっていくので安心です。

ブログの記事にまとめてみると、そんなに大変な修正でもなかったな?という感じですが、当時は久々に PHP 触ったりするのもあり、結構大変でした。

今後は phpstan とか入れたりして、コードの品質も上げていければなと思っています。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 4

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 3 の続編となります。

PHP 8.2 で実行できるように修正していく

ひとまずロジック部分については 8.2 環境で PHPUnit でテストできるようになり、CI も動作するようになったので、アプリケーションを起動して動くのかを試していきたいと思います。

Memcache の残りを移行する

Memcache は Redis に移行したのですが、単体テスト外の部分にも少し残っていたので、こちらのコミット で対応しました。

移行方法は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2 でも実施した内容なのですが、DI 部分と設定ファイルのデフォルト値、READMEの説明を修正しました。

GAE のアプリケーションバージョンを新しく設定する

GAE では通常のトラフィックを古いバージョンへ向けて、新しいバージョンもデプロイして別のURLから実行できるようにする機能があります。 dev_appserver.py での実行が困難になっているので、新しいバージョンを GAE にデプロイして動作するのかを検証していくことにしました。

今回はバージョン2として修正しました。

GAE で動かすにあたり php.ini も変更が必要だったので、拡張に redis を追加して修正しました

デプロイして動かしてみる

全然動きませんな (^^;;

まず問題を切り分けるために、まだビジネスロジックに分岐できていなかったデバイス値の取得処理を Google Cloud Storage から取得する処理をテスト可能なサービスとしてリファクタリングしました。

あとセッションの保存場所を設定する必要があったので、いったん php.inisession.save_path=Google\AppEngine\Api\Memcache\Memcache を追加しました。

依存のライブラリもいくつか PHP 8.2 環境だと動かないものがあったのでアップデート。

いったんそれらの修正コミットがこちら

で、いろいろやっていくうちに、そもそも slim3 だと PHP 8系で動かないな…という基本的なところに気づきました(さいしょから考えておけよという話)。

Slim3 から Slim4 に移行していく

幸いなことにこちらに関してはインターネットに移行記事がたくさんあり、とても参考になりました。

今回の移行に関するコミットがこちらです。

依存関係の更新

  • slim 3.1 を 4.12 に
  • monolog のアプデ
  • BASIC認証のライブラリ tuupola/slim-basic-auth のアプデ
  • セッションミドルウェア akrabat/rka-slim-session-middleware のアプデ
  • PSR7実行が外部依存になったので slim/psr7 の追加
  • DI が外部依存になったので php-di/php-di の追加
  • Google Cloud Storage がランタイム外になったので google/cloud-storage の追加

php-di への移行

もともとは Slim3 のアプリケーションコンテナを使っていたので、こんな感じでやっていたのを

// Instantiate the app
$settings = require __DIR__ . '/src/settings.php';
$app = new \Slim\App($settings);

以下のように変更しました。

use DI\Container;

$settings = require __DIR__ . '/src/settings.php';
$container = new Container();
$container->set('settings', $settings['settings']);

続いてDIコンテナの初期化処理化を

require __DIR__ . '/src/dependencies.php';

のように指定していたのを

use Slim\Factory\AppFactory;

$dependencies = require __DIR__ . '/src/dependencies.php';
$container = $dependencies($container);

AppFactory::setContainer($container);
$app = AppFactory::create();

このように変更しました。

  • コンテナを初期化
  • 環境設定の注入やコンテナの初期化を実行
  • AppFactory でアプリケーション初期化

のような実装に変わります。

Slim4 っぽい書き方に変更

もともとは

// Register middleware
require __DIR__ . '/src/middleware.php';

// Register routes
require __DIR__ . '/src/routes.php';

こんな感じでミドルウェアとルーティングの設定を書いていたのですが、

// Register middleware
$middleware = require __DIR__ . '/src/middleware.php';
$middleware($app);

// Register routes
$routes = require __DIR__ . '/src/routes.php';
$routes($app);

Slim4アプリを引数で指定して設定するように変更しました。 $app をグローバル参照から引数参照するコールバック関数に変わったぐらいです。

Basic認証の修正

登録済みのデバイスからデータを受信するAPIでは、Basic認証ミドルウェアを設定していて、 callback という処理が呼び出されることになっていました。

//
// For Registered Devices
//
$app->post('/api/devices/{id}/values', '\ToiletEvolution\Controllers\DeviceValuesController:add')
    ->add(new \Slim\Middleware\HttpBasicAuthentication([
        "authenticator" => new ToiletEvolution\Middlewares\HttpBasicAuthentication\DeviceAuthenticator($app->getContainer()->get('DeviceStore')),
        "callback" => function($request, $response, $arguments) {
          $route = $request->getAttribute('route');
          $id = $route->getArgument('id');
          return $id === $arguments['user'];
        },
        "secure" => false
      ]));

Slim4でのBasic認証では before が呼び出される様に変わっています。

  //
  // For Registered Devices
  //
  $app->post('/api/devices/{id}/values', '\ToiletEvolution\Controllers\DeviceValuesController:add')
      ->add(new \Tuupola\Middleware\HttpBasicAuthentication([
          "authenticator" => new ToiletEvolution\Middlewares\HttpBasicAuthentication\DeviceAuthenticator($app->getContainer()->get('DeviceStore')),
          "before" => function($request, $response, $arguments) {
            $id = $arguments('id');
            return $id === $arguments['user'];
          },
          "secure" => false
        ]));

コントローラーの修正

APIでJSONを返却するときの処理が標準ではなくなったので、JSON文字列を出力する JsonRenderer のようなクラスを作ります。

namespace ToiletEvolution\Renderer;

use Psr\Http\Message\ResponseInterface;

final class JsonRenderer
{
    /**
     * Write JSON to the response body.
     *
     * This method prepares the response object to return an HTTP JSON
     * response to the client.
     *
     * @param ResponseInterface $response The response
     * @param mixed $data The data
     *
     * @return ResponseInterface The response
     */
    public function json(
        ResponseInterface $response,
        mixed $data = null,
    ): ResponseInterface {
        $response = $response->withHeader('Content-Type', 'application/json');

        $response->getBody()->write(
            (string)json_encode(
                $data,
                JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR
            )
        );

        return $response;
    }
}

で、それをコントローラのコンストラクタで初期化して、APIメソッドで呼び出します。

class DevicesController
{
  protected ContainerInterface $ci;
  private JsonRenderer $renderer;

  public function __construct(ContainerInterface $ci)
  {
    $this->ci = $ci;
    $this->renderer = new JsonRenderer();
  }

  public function index(ServerRequestInterface $request, ResponseInterface $response, array $args)
  {
    $deviceModel = $this->ci->get(Device::class);

    $data = array_map(function($device) {
      return $device->toArrayWithoutSecret();
    }, $deviceModel->all());

    return $this->renderer->json($response, $data)->withStatus(empty($data)?204:200);
  }

Slim3 では以下のようなコードで $response->withJson でよかったところが変更になっています。

public function index($request, $response, $args)
{
    $deviceModel = $this->ci->get(Device::class);

    $data = array_map(function($device) {
      return $device->toArrayWithoutSecret();
    }, $deviceModel->all());

    return $response->withJson($data, empty($data)?204:200);
}

ミドルウェアの修正

PSR の書き方が変わっているので、それに応じて修正しています。

class RequireLoginMiddleware
{
    public function __invoke($request, $response, $next)
  {
    if(empty($this->session->get('current_user')))
    {
      $response = $response
        ->withStatus(302)
        ->withHeader('Location', $this->redirectIfNotLogin);
    }
    else
    {
      $response = $next($request, $response);
    }

    return $response;
  }    

こんな感じだったのが、以下のように変更になっています。

class RequireLoginMiddleware
{
  public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
  {
    $response = new Response();
    if(empty($this->session->get('current_user')))
    {
      $response = $response
        ->withStatus(302)
        ->withHeader('Location', $this->redirectIfNotLogin);
    }
    else
    {
      $response = $handler->handle($request);
    }

    return $response;
  }

引数が変更になっていて、 $response がなくなって、次のミドルウェアへ処理を引き継ぐのが $next から $handler に変わっています。

$response はミドルウェアごとに作成して設定していきます。 次のミドルウェアに引き継ぐときは、 $handler->handle で呼び出してレスポンスを取得します。

さいごに

Slim3 から Slim4 への移行手順がインターネットに多くあって助かりました。 また Slim 関連の OSS パッケージも 3 から 4 への移行をドキュメントにしてくれていたので、比較的スムーズに移行できたと思います。

ここではその一部を紹介する感じになりましたが、同じような対応をする人に役立つ内容になっていれば嬉しいです。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 3

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2 の続編となります。

前回は PHPUnit を最新化して通過するところまで実施したので、今回は GitHub Actions で CI できるようにしていきます。

GitHub Actions と Google Cloud を連携する

Authenticate to Google Cloud from GitHub Actions というリポジトリがあって、Actions から gcloud 関係の CLI ツールを動かす前に認証を通過させる方法が書いてあります。

Actionsが通過するようになったPRがこちら

GitHub Actions からのキーなしの認証の有効化 という公式ドキュメントに加え、先ほども紹介したリポジトリのREADMEを併せて読むと簡単に環境構築できるようになります。

環境構築ができたら Actions の yaml ファイルを定義していくだけです。

  • ソースコードをチェックアウト
  • google-github-actions/auth で認証
  • google-github-actions/setup-gcloud で gcloud コマンドをセットアップ
  • PHP8.2環境のセットアップ
  • composer アクションで依存関係を解決
  • gcloud コマンド実行
  • PHPUnit 実行

のような手順にしました。

さいごに

google-github-actions/auth の認証方法が変更になっているので最新に追従しないといけないので、おいおい対応していきます。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 1 の続編となります。

単体テストを通過するようにアップグレード

やることは多そうだなというのは想像していたのですが、いざ取り掛かろうとしたとき頼りになるのは単体テストだな、ということでPHPUnitとPHPのバージョンを上げて、テストが通過するように修正していきます。

テストが通過するようになったコミットがこちら

composer の依存バージョンを変更

  • PHPのバージョンを8.2以上へ
  • PHPUnitのバージョンを10.1.3(当時最新)へ

その関連で合わせてあげないといけない依存もバージョンup。

PHPUnit の実行方法を変更

以前は GAE の内部コンポーネントに依存していたので、PHPUnitを実行するのも dev_appserver.py を起動してからそのインスタンス内の PHPUnit 実行スクリプトを経由して実行していたのですが、GAE gen2 ではPHPスクリプトの起動自体が php -S に変わったりしたこともあり、PHPUnitもComposer スクリプトから直接実行するように変更しました。

以前は dev_appserver 経由で起動されていたデータストアとキャッシュについては、docker と gcloud emulator で起動してから PHPUnit を実行するように変更しました。

実行手順をREADMEにも記述。

ここで、レガシーランタイム脱却のため Memcache を Redis に変更する対応も入っています。 当初は Memcache を変更せずに PHPUnit が通過してから移行する予定だったのですが、 GeckoPackages\MemcacheMock が終了していて、最新のPHP/PHPUnitで動作しないことがわかったのでこのタイミングで一緒に移行を決断しました。

簡単なアーキテクチャと既存テストコード

Toilet Evolution は PHP Slim Framework に、Google Datastore を使用(DatastoreモードのFirestoreを使用)しています。 REST API を通じてデバイスからトイレの利用状況をデータストアやキャッシュに格納したり、Webアプリから利用状況を閲覧できるAPI が用意されています。

テストコードは、データストアとキャッシュのI/O、PSRのミドルウェア部分にあって、主なロジックはこのあたりに集中しています。

テストコードの記述を修正

PHPUnit のテストケース(クラス)の宣言を修正

// PHP5.5 / PHPUnit 4.8
class DeviceTest extends \PHPUnit_Framework_TestCase {}

// PHP8.2 / PHPUnit 10.1.3
use PHPUnit\Framework\TestCase;

class DeviceTest extends TestCase {}

setUp を before に変更

// PHP5.5 / PHPUnit 4.8
public function setUp()

// PHP8.2 / PHPUnit 10.1.3
/**
 * @before
 */
public function before()

assert を $this 経由で呼び出すように変更

// PHP5.5 / PHPUnit 4.8
assertEquals('username', $results->name);

// PHP8.2 / PHPUnit 10.1.3
$this->assertEquals('username', $results->name);

Memcache から Redis への記述を修正

インスタンスはDIしているので、変数名を memcache から redis に変更した。 メソッドは get / set で同じになるけど、データがオブジェクト形式から文字列に変わるので以下のような対応となった

// GAE gen1
$this->memcache->set("device:id:{$device->getKeyId()}", $device);

// GAE gen2
$this->redis->set("device:id:{$device->getKeyId()}", serialize($device));

データを set するときは serialize して、 get するときは unserialize するように変更している。

このぐらいで PHPUnit がうごくようになった

Warining は出てるんですが、PHPUnit がエラーで落ちることはなくなりました。 これはバージョンアップ意外と楽チンなのでは?!と勘違いしたのは言うまでもなく、この連載もまだ続きます。

ここで勘違いしたので、心も折れず続けられたというのはあるかもしれないと振り返り思うのでした。