Technote

by sizuhiko

uvu での Test Double

uvu は軽量で高速なJavaScript/TypeScriptのテスティングランナーです。 SveltteKit でも利用されており、pong-swoosh でも採用しています。

現時点の uvu は assert 機能がメインで、依存関係をモックして置き換えるような Test Double には対応していません。 ただ、すべてが DI で解決できるわけではなく、依存パッケージについてはモックしたい場合もあります。

そこで pong-swoosh では、以下の2つのライブラリを使って、 uvu で Test Double を実現しています。

snoop

Easy breezy test spies fo sheezy. というキャッチです(よくわからんw)。

やれることは Jest でいうところの Jest.fn() だと思っていれば大丈夫です。 モック関数を作れます。公式の Example でも uvu をランナーに使っていることから、親和性も高いです。

import { snoop } from 'snoop';
import { test } from 'uvu';
import * as assert from 'uvu/assert';

const add = (a, b) => a + b;

test('add', () => {
  const addFn = snoop(add);
  const anotherAddFn = snoop(add);

  addFn.fn(1, 1);
  anotherAddFn.fn(1, 1);
  addFn.fn(2, 2);
  anotherAddFn.fn(2, 2);

  assert.ok(addFn.called);
  assert.not(addFn.notCalled);
  assert.not(addFn.calledOnce);
  assert.is(addFn.callCount, 2);
  assert.equal(addFn.calls[0].arguments, [1, 1]);
  assert.equal(addFn.calls[1].arguments, [2, 2]);
  assert.is(addFn.calls[0].result, 2);
  assert.is(addFn.calls[1].result, 4);
  assert.not(addFn.firstCall.error);
  assert.not(addFn.lastCall.error);
  assert.ok(addFn.calledBefore(anotherAddFn));
  assert.ok(addFn.calledAfter(anotherAddFn));
  assert.ok(addFn.calledImmediatelyBefore(anotherAddFn));
  assert.not(addFn.calledImmediatelyAfter(anotherAddFn));
});

test.run();

proxyquire

Node.js の require モジュールをプロキシすることができるライブラリです。 Jest でいうところの Jest.mock('ライブラリ名', {/** モック */}) だと思っていれば大丈夫です。

公式サンプルを引用すると、以下のような感じです。

get.js:

var get    = require('simple-get');
var assert = require('assert');

module.exports = function fetch (callback) {
  get('https://api/users', callback);
};

get.test.js:

var proxyquire = require('proxyquire').noCallThru();
var assert = require('assert');

var fetch = proxyquire('./get', {
  'simple-get': function (url, callback) {
    process.nextTick(function () {
      callback(null, { statusCode: 200 })
    })
  }
});

fetch(function (err, res) {
  assert(res.statusCode, 200)
});

pong-swoosh での使い方

この2つを組み合わせて、依存モジュールを proxyquire で置き換え、置き換える関数は snoop で作っています。

create-channel.jsのテストコードを例にします。 create-channel は、ブラウザからのチャンネル作成リクエストを受け付けたあと、実際にチャンネルを作成する処理です。

test('まだ作成されていないチャンネルの場合は、チャンネル追加されてソケットにオーナー登録されること', (context) => {
  const getChannelMock = snoop(() => false); // 存在しないチャンネル
  const addChannelMock = snoop(() => {});
  const channelId = faker.datatype.uuid();
  const createChannel = proxyquire('../create-channel', {
    './channel': {
      getChannel: getChannelMock.fn,
      addChannel: addChannelMock.fn,
    },
    'uuid': {
      v4: () => channelId,
    },
  });
  const result = createChannel(context.socket, 'test', 'test.ch');

  assert.ok(addChannelMock.called);
  assert.is(addChannelMock.callCount, 1);
  assert.equal(addChannelMock.calls[0].arguments, ['test', channelId, 'test.ch']);

create-channel の実装では以下のように channel モジュールに依存しているので、そこをモックしていきます。

const channel = require('./channel');
const { v4: uuidv4 } = require('uuid');

module.exports = (socket, userId, channelName, channelId) => {
  const exists = channel.getChannel(userId, channelName);

まず、Test Double を下準備します。

  const getChannelMock = snoop(() => false); // 存在しないチャンネル
  const addChannelMock = snoop(() => {});
  const channelId = faker.datatype.uuid();
  const createChannel = proxyquire('../create-channel', {
    './channel': {
      getChannel: getChannelMock.fn,
      addChannel: addChannelMock.fn,
    },
    'uuid': {
      v4: () => channelId,
    },
  });

snoop でモック関数を2つ作り、それを proxyquire を使って、 ./channelrequire モジュールを置き換えます。 uuid モジュールも同様に snoop でモックしていることがわかるでしょう。

で、実際に const result = createChannel(context.socket, 'test', 'test.ch'); でテスト対象のモジュールを呼び出したら getChannelfalse になると addChannel が呼ばれるのが正しい動作なので、以下のように検証しています。

  assert.ok(addChannelMock.called);
  assert.is(addChannelMock.callCount, 1);
  assert.equal(addChannelMock.calls[0].arguments, ['test', channelId, 'test.ch']);
  • 1行目は、 addChannel が呼ばれたかどうか
  • 2行目は、 addChannel が何回呼ばれたか
  • 3行目は、 addChannel がどのような引数で呼び出されたか

を検証することができます。Jest で Test Double を書いたことがあれば、イメージはつきやすいと思います。 このように Test Double の機能が標準では備わってない uvu でも、どうしてもモックしたいテストも記述できるようになっています。

GitHub Enterprise の Actions で依存関係を S3 にキャッシュする

GitHub の Actions で npm などの依存関係パッケージをキャッシュするのは、公式ドキュメント 依存関係をキャッシュしてワークフローのスピードを上げる にも書いてあるように、 CIを高速化するのに重要です。

公式 Example だと以下のとおりです。

name: Caching with npm

on: push

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Cache node modules
        uses: actions/cache@v3
        env:
          cache-name: cache-node-modules
        with:
          # npm cache files are stored in `~/.npm` on Linux/macOS
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-

      - name: Install Dependencies
        run: npm install

      - name: Build
        run: npm build

      - name: Test
        run: npm test

actions/cache を利用すれば問題なく動くのですが、Enterprise 版の場合は runs-on にセルフホストランナーを利用するので、キャッシュがホストランナーのディスクに依存してしまいます。 これだとホストランナーのディスク管理とか大変になるので、少し管理で楽をしたいな、と思ったところ S3 にキャッシュできるものがあり、現在もこれを利用しています。

actions-s3-cache

actions-s3-cache は名前のとおり依存関係のキャッシュをS3に保持してくれます。 これならディスク容量を気にすることもないし、管理が楽になりますね。

公式 Example だと、こんな感じです。

steps:
  - uses: actions/checkout@v2
  - uses: shonansurvivors/actions-s3-cache@v1.0.1
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: us-east-1
    with:
      s3-bucket: your-s3-bucket-name # required
      cache-key: npm-v1-${{ hashFiles('laravel/package-lock.json') }} # required ('.zip' is unnecessary)
      paths: node_modules # required 
      command: npm ci # required
      zip-option: -ryq # optional (default: -ryq)
      unzip-option: -n # optional (default: -n)
      working-directory: laravel # optional (default: ./)

僕らの運用では

  • S3バケットは複数リポジトリで共有
  • キー名にリポジトリ名を入れる

みたいな決め事を作ったので、こんな感じにしてみました。

  - uses: actions/checkout@v2
  - id: get-repository-name
    run: |
      IFS=/
      REPO=${{ github.repository }}
      set -- ${REPO}
      echo "::set-output name=name::$2"
  - uses: shonansurvivors/actions-s3-cache@v1.0.1
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: ap-northeast-1
    with:
      s3-bucket: ci-actions-cache
      cache-key: ${{steps.get-repository-name.outputs.name}}-cache-${{ hashFiles('package-lock.json') }}
      paths: node_modules
      command: npm ci

get-repository-name でリポジトリ名を抜き出してきて、 cache-key で使っています。 cache-key に一致するものがあれば、S3からダウンロードしてzipを展開、一致するものがなければ command を実行して paths にあるファイルを zip アーカイブして S3 に保持といったことをやってくれるので、便利ですね。

あとは S3 のバケットにファイルの有効期限を設定しておくようにすれば、古いキャッシュがいつまでも残ることもなく、とても便利です。

TypeORM でプライマリーキーや外部キーの名前を変更する

TypeORM では、主キーやインデックスはエンティティにデコレータを記述します。 たとえば以下のような感じです。

@Entity('テーブル名')
export class UserEntity {
  @PrimaryGenerationColumn()
  id!: string;
}

@PrimaryGenerationColumn にはインデックス名を指定できないので、この状況で migration:generate を実行すると、 TypeORM 独自のルールを使ったインデックス名で SQL が生成されます。

一方でインデックスの場合は、たとえば email にユニーク制約を入れるとすると以下のように、インデックス名を指定できます。

@Entity('テーブル名')
export class UserEntity {
  @PrimaryGenerationColumn()
  id!: string;
  @Index('UQ_User_email', {unique: true})
  @Column()
  email!: string;
}

また関連に関してもそうで、 User - hasMany -> Post のような関連があったとき、外部キー制約の名前も TypeORM ルールになります。

@Entity('テーブル名')
export class UserEntity {
  @PrimaryGenerationColumn()
  id!: string;
  @Index('UQ_User_email', {unique: true})
  @Column()
  email!: string;
  @OneToMany(() => PostEntity, (post) => post.user)
  posts?: PostEntity[];
}

@Entity('テーブル名')
export class PostEntity {
  @PrimaryGenerationColumn()
  id!: string;
  @ManyToOne(() => UserEntity, (user) => user.posts)
  user!: UserEntity;
}

そこで、 @Index 以外で生成されるインデックスの名前をカスタマイズしたい!という場合は NamingStrategy を使います。 この NamingStrategy ですが、ドキュメントには詳しい解説がないので、この記事を書くことにしました。

NamingStrategy の作り方

まずはインデックス名をカスタムできるように、 DefaultNamingStrategy を継承し、 NamingStrategyInterface を実装するクラスを作ります。

class CustomNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
}

DefaultNamingStrategyのコード を参考にカスタマイズすると良いでしょう。

たとえばプライマリーキーの場合は以下のようになっています。

  primaryKeyName(tableOrName: Table | string, columnNames: string[]): string {
    // sort incoming column names to avoid issue when ["id", "name"] and ["name", "id"] arrays
    const clonedColumnNames = [...columnNames]
    clonedColumnNames.sort()
    const tableName = this.getTableName(tableOrName)
    const replacedTableName = tableName.replace(".", "_")
    const key = `${replacedTableName}_${clonedColumnNames.join("_")}`
    return "PK_" + RandomGenerator.sha1(key).substr(0, 27)
  }

プライマリーキーに設定されているカラム名をソートしてから _ で結合して sha1 に変換したものを利用しています。

たとえばプロジェクトのルールで、プライマリーキーは PK_テーブル名 で良い場合は、以下のようなコードでオーバーライドして変更します。

class CustomNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
  primaryKeyName(tableOrName: Table | string, columnNames: string[]): string {
    const table = this.getTableName(tableOrName);
    return `PK_${pascalCase(table)}`;
  }
}

pascalCasechange-case のようなライブラリ利用を想定しています。

NamingStrategy で変更できる名前

NamingStrategyInterfaceのコードを見るとわかります。 よく使いそうなものは、以下のあたりでしょうか。

  • primaryKeyName 主キー名。デフォルトでは PK_{sha1した値}
  • foreignKeyName 外部キー名。デフォルトでは FK_{sha1した値}
  • indexName 複合キー名。デフォルトでは IDX_{sha1した値}

もちろんこれ以外にもたくさんカスタマイズできる名前はあるので、マイグレーションファイルに生成された名称がプロジェクトルールに沿わない場合に 遭遇したら、インターフェースやデフォルトの実装を見てみると良いでしょう。

カスタム NamingStrategy の指定方法

マイグレーションコマンドから利用する DataSource のオプションに指定します。

export const AppDataSource = new DataSource{
  // データベース接続オプション
  migrations: ['db/migrations/*.ts'],
  namingStrategy: new CustomNamingStrategy()
});

namingStrategy を指定して、 npm run migration:generate するとプロジェクトルールに沿った名前でマイグレーションファイルを生成できます。

package.json のスクリプト例

  "migration:generate": "ts-node ./node_modules/.bin/typeorm migration:generate -d db/datasouce.ts db/migrations",

さいごに

公式ドキュメントの情報が不足しているところなので、この記事が少しでも役立てば幸いです。

TypeORM を 0.3 系にアップグレードする

TypeORM 0.2 系をずっと使ってきて、 2022/03/23 の Dependabot 更新で、 0.3 系へのアップデートを確認しました。 CIのテストが失敗していたので、何か大きな変更があるのか Release notes を確認してみました。

すると、 0.3.00.3.1 の2度に BREAKING CHANGES があることがわかりました。 ただし後者 0.3.1 の BREAKING CHANGES については、 0.3.2 で元に戻されているので 0.3.0 の変更点を見ていきましょう。

0.3 系での変更点

  • ormconfig のような接続オブションファイル利用は非推奨になった
  • 非推奨の migrations:* コマンドが削除された
  • CLI コマンドの前面見直し
  • 未実行のマイグレーションとスキーマ同期の両方がある場合、マイグレーションがスキーマ同期の前に実行されるようになった
  • aurora-data-api が aurora-mysql に変更
  • aurora-data-api-pg が aurora-postgres に変更
  • EntityManager.connection が EntityManager.dataSource に変更
  • Repository のコンストラクタが新しくなった。カスタムクラスリポジトリは使えなくなった
  • @TransactionRepository、@TransactionManager、@Transaction デコレータが削除された
  • ジャンクションテーブル名だけが短縮されるようになった(@Entityデコレータでカスタム名を指定するのが理想的
  • パラメータなしの findOne() は廃止
  • findOne(id) は削除
  • findOne, findOneOrFail, find, count, findAndCount メソッドは FindOptions パラメータのみに変更。その代わり FindOptions を使わずに where 条件を直接与えるために、新しいメソッドが追加された: findOneBy, findOneByOrFail, findBy, countBy, findAndCountBy
  • findByIdsは非推奨となり、代わりにIn演算子と組み合わせてfindByメソッドを使用することが推奨となる
  • findOne と QueryBuilder.getOne() は、データベースで何も見つからなかった場合、undefined ではなく null を返すようになった
  • find* メソッドで使用される where の値として null はサポートされず、IsNull() 演算子を明示的に使用するようになった
  • すべてのCLIコマンドはormconfigをサポートしなくなった
  • DataSourceOptions 内のエンティティ、マイグレーション、サブスクライバのオプションにディレクトリ文字列を指定するのは非推奨になった
  • すべてのコンテナ関連の機能 (UseContainerOptions, ContainedType, ContainerInterface, defaultContainer, useContainer, getFromContainer) は非推奨となった
  • トランザクション内で使用される EntityManager の getCustomRepository は、非推奨となった。代わりに withRepository メソッドを使用する
  • Connection.isConnected は非推奨になった。代わりに .isInitialized を使用する
  • FindOptions (find* メソッドで使用) の select や relations でプロパティ名の配列指定は非推奨となった。代わりにオブジェクトリテラル表記を使用する
  • FindOptions の join (find* メソッドで使用されます) は非推奨になった。結合を含むクエリを作成するには、QueryBuilder を使用する
  • Connection、ConnectionOptions は非推奨になった。DataSource と DataSourceOptions を使用する
  • createConnection(), createConnections() は非推奨になった。Connection は DataSource になった
  • getConnection() は非推奨になった。グローバルな接続を持つには、データソースをエクスポートして必要な場所でそれを使用する
  • getManager(), getMongoManager(), getSqljsManager(), getRepository(), getTreeRepository(), getMongoRepository(), createQueryBuilder() はすべて非推奨になった。これらは DataSource から取得する
  • getConnectionManager() と ConnectionManager は非推奨になった。Connection は DataSource になり、各データソースは変数としてエクスポートできる
  • getConnectionOptions() は非推奨になった
  • AbstractRepository は非推奨になった
  • Connection.name と BaseConnectionOptions.name は非推奨になった

めっちゃ、いっぱいありますね。

修正箇所の確認

修正箇所に関して、型が変わったものに関しては、 tsc でコンパイルすればエラーになるので、対応箇所は明確です。 findオプションが変わったり、メソッド自体が無くなったりしたものが中心です。

コンパイルエラーが修正できたら、あとは単体テストでの失敗箇所の確認ですね。

非推奨になった部分にも目を向ける

最新のドキュメントは非推奨になった記述がすべてなくなり(そりゃそうですが)、新しい記述方法に変わっているのと、いつ 0.4.0 になっても良いように、 どのように修正したら対応できるかを考えておく必要があります。

また、最近新しいAPIサーバーを作ることになったので、その実装は 0.3 系で始めるようにしました(あえて古いバージョンを使う必要はないですしね)。 で、そこで問題になるのが、周辺ライブラリの対応状況です。 TypeORM に対応したテストデータ作成のライブラリはいくつかあるのですが、そういったものが 0.3 系に追従してくるまで待つか、そもそも依存ライブラリを使わずに faker-ts のような TypeScript のモックデータを作れるライブラリと TypeORM の create/save を組み合わせて使うかの選択になります。 今回の新しいAPIサーバーでは、後者のやり方で進めることにしました。いずれライブラリの対応が進んだとして切り替えるか、そのままにするかは検討の余地がありますが、それほど手間は変わっていない印象です。

ここからは、昨日の記事でもふれたのですが、私が関わっているプロジェクトでは DI フレームワークとして tsyringe を、 ORM で typeorm を使っていますので、その範囲でどのように記述しているかを紹介していきます。

Connection から DataSource への変更に対応する

TypeORM 0.2 系では以下の書き方が一般的でした。

if (getConnectionManager().has('default')) {
  const conn = getConnectionManager().get('default');
  if (conn.isConnected === false) {
    await conn.connect();
  }
} else {
  await createConnection({ /* 接続オプションの指定 */ });
}

これが 0.3 系では以下のようになります。

let ds: DataSource;
if (container.isRegistered('DataSource')) {
  ds = container.resolve('DataSource');
} else {
  ds = new DataSource({ /* 接続オプションの指定 */});
  container.register('DataSource', { useValue: ds });
}
if (!ds.isInitialized) {
  await ds.initialize();
}

データソースのオブジェクトは tsyringe の DI コンテナに登録して利用できるようにしておきます。 さらに DB(リポジトリ)を、ユースケースクラスで DI できるようにします。

TypeORM 0.2 系では以下のようにしていました。

container.register('UserRepository', {
  useFactory: instanceCachingFactory(() => getCustomRepository(UserDatabase))
})

TypeORM がグローバルにアクセスできる getCustomRepository を用意してくれていたので、カスタムリポジトリクラスを指定するだけで良かったのですが、 ここでは getCustomRepository が非推奨になったのと、クラスベースのカスタムリポジトリが非推奨になった、という2つの非推奨の影響を受け、書き方がだいぶ変わりました。

0.3 系では以下のようにしてみました。データソースを DI コンテナから取得して新しいカスタムリポジトリの書き方 extend を使っています。

container.register('UserRepository', {
  useFactory: instanceCachingFactory(() => {
    const ds = container.resolve('DataSource');
    return ds.getRepository(UserEntity).extend(UserDatabase);
  }
})

クラスベースのカスタムリポジトリからオブジェクトベースへの変更に対応する

で、 0.2 系でのカスタムリポジトリは以下のようにクラスベースになっていたのです。

@EntityRepository(UserEntity)
export class UserDatabase extends Repository<UserEntity> implements UserRepository {
  async findByEmail(email: string): Promise<UserEntity | undefined> {
    // 検索処理
  }
}

これを 0.3系では、クラスベースでなくオブジェクトベースに変更します。

export const UserDatabase: UserRepository & ThisType<Repository<UserEntity> & UserRepository> =
{
  async findByEmail(email: string): Promise<UserEntity | null> {
    // 検索処理
  },
} 

クラスベースからオブジェクトベースになって、とても困ったことがあります。 上記の変更では簡略化しましたが、実は find 系メソッドにはデコレータをつけていて、ロギングできるような仕組みを入れていました。

@EntityRepository(UserEntity)
export class UserDatabase extends Repository<UserEntity> implements UserRepository {
  @Logging()
  async findByEmail(email: string): Promise<UserEntity | undefined> {
    // 検索処理
  }
}

こんな感じです。 これがオブジェクトベースになると、デコレータが使えなくなります。 いい感じの解決策はあまりなかったので、AOPのライブラリ ts-aspect を使うことにしました。 AOPのライブラリはnpmレジストリにたくさんあるので、どれか自分の好みにあうものを利用すれば良いと思います。

で、0.3 系ではデコレータを使わず、AOPで以下のように対応しました。

container.register('UserRepository', {
  useFactory: instanceCachingFactory(() => {
    const ds = container.resolve('DataSource');
    const repository = ds.getRepository(UserEntity).extend(UserDatabase);
    addAspect(repository, 'findByEmail', Advice.Around, new LoggingAspect());
    return repository;
  }
})

カスタムリポジトリをインスタンス化するときに、いったん変数 repository にしてから ts-aspect を使って、メソッド単位で仕込んでいきます。

マイグレーションやCLIの変更に対応する

CLIオプションが変更になっているので、マイグレーションの書き方も変更が必要です。 0.2 系では package.jsonscripts にこんな感じで書いていたと思います。

  "migration:generate": "ts-node ./node_modules/.bin/typeorm migration:generate -f db/ormconfig.ts -n Migration",

ormconfig.ts の中身は以下のような感じでした。

module.exports = {
  // データベース接続オプション
  migrations: ['db/migrations/*.ts']
  cli: {
    entitiesDir: 'domain/entities',
    migrationsDir: 'db/migrations',
    subscribersDir: 'db/subscribers',
  }
};

0.3 系では、まずスクリプトが以下のように変更になります。

  "migration:generate": "ts-node ./node_modules/.bin/typeorm migration:generate -d db/datasouce.ts db/migrations",

-f オプションがデータソース -d 指定となり、マイグレーションファイルの出力先を最後に指定するようになりました。 datasource.ts は以下のようになります。データソースに変わった関係で、 cli オプションはなくなっています。

export const AppDataSource = new DataSource{
  // データベース接続オプション
  migrations: ['db/migrations/*.ts']
});

migration:run や migration:show は、データソースに指定した migrations のパスからファイルを探索してくれるので、 -f-d に変わったぐらいの影響範囲で大丈夫です。

さいごに

typeorm は 0.2 -> 0.3 -> 0.4 と 0.x 系なので 0.1 ごとに破壊的変更が実施されてきます。 バージョン 2.0 -> 3.0 -> 4.0 にすれば良いのに… とか思いますが。

次の 0.4 系に備えて、修正の方針を見つけていきたいですね。 もしこの記事が参考になれば幸いです。

DependabotをGHEのActionsとPackagesで利用する

以前に DependabotをGHEのプロジェクトに適用する という記事を書きましたが それから状況は変わり CI は drone.io から GHE の Actions を使うように、プロジェクト内のプライベートパッケージは Packages に入れるようになりました。

すべて GHE のプラットフォームに寄せられたのは良かったのですが、現時点まだ Dependabot はβ版のようなので、ひとまずセルフホストランナーを使って実行できるようにしてみました。

name: dependabot
on:
  workflow_dispatch:
  schedule:
    - cron: 'お好みのスケジュールで'
jobs:
  dependabot:
    runs-on: self-hosted
    env:
      PROJECT_PATH: ${{ github.repository }}
      BRANCH: develop
      PACKAGE_MANAGER: npm_and_yarn
      GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_ACCESS_TOKEN }}
      GITHUB_ENTERPRISE_ACCESS_TOKEN: ${{ secrets.GITHUB_ENTERPRISE_ACCESS_TOKEN }}
      GITHUB_ENTERPRISE_HOSTNAME: ${{ secrets.GITHUB_ENTERPRISE_HOSTNAME }}
      NPM_REGISTRY_URL: npm.github.hoge.com
      NPM_TOKEN: ${{ secrets.PACKAGES_DOWNLOAD_TOKEN }}
    steps:
      - run: docker pull dependabot/dependabot-core
      - name: run dependabot
        run: |
          rm -rf dependabot-script
          git clone https://foo:${GITHUB_ACCESS_TOKEN}@github.com/sizuhiko/dependabot-script.git
          cd dependabot-script
          docker run -v "$(pwd):/home/dependabot/dependabot-script" \
          -w /home/dependabot/dependabot-script dependabot/dependabot-core bundle install -j 3 --path vendor
          docker run --rm -v "$(pwd):/home/dependabot/dependabot-script" \
          -w /home/dependabot/dependabot-script \
          -e PROJECT_PATH=${PROJECT_PATH} \
          -e BRANCH=${BRANCH} \
          -e PACKAGE_MANAGER=${PACKAGE_MANAGER} \
          -e GITHUB_ACCESS_TOKEN=${GITHUB_ACCESS_TOKEN} \
          -e GITHUB_ENTERPRISE_ACCESS_TOKEN=${GITHUB_ENTERPRISE_ACCESS_TOKEN} \
          -e GITHUB_ENTERPRISE_HOSTNAME=${GITHUB_ENTERPRISE_HOSTNAME} \
          -e NPM_REGISTRY_URL=${NPM_REGISTRY_URL} \
          -e NPM_TOKEN=${NPM_TOKEN} \
          dependabot/dependabot-core bundle exec ruby ./generic-update-script.rb

GHE では self-hosted ランナーを使うのが一般的なのですが、そこに dependabot を入れるのではなく、 dependabot-core の Docker イメージを使って実行できるようにします。 これは以前の記事と同じ実行方法ですね。

で、drone.io の場合と異なり Actions で該当のリポジトリで cron ビルドするようにしています。 cron のスケジュールは日次なり、週次なりで良いでしょう。

設定する環境変数

以下の環境変数でコントロールしています。

  • BRANCH PRの向き先のブランチ名。git flow の場合は develop
  • PACKAGEMANAGER パッケージマネージャ名。npmやyarnの場合は npmand_yarn
  • GITHUBACCESSTOKEN GitHubのアクセストークン。GHEのシークレットに入れておくと良いです
  • GITHUBENTERPRISEACCESS_TOKEN GHEのアクセストークン。GHEのシークレットに入れておくと良いです
  • GITHUBENTERPRISEHOSTNAME GHEのホスト名。GHEのシークレットに入れておくと良いです
  • NPMREGISTRYURL プライベートレジストリのURL
  • NPM_TOKEN パッケージダウンロード権限のあるPAT(GHEのPersonal Access Token)。GHEのシークレットに入れておくと良いです

dependabot-script を fork してみた

さて、あとは dependabot-core のイメージ内で dependabot-script を動かせば良いのですが、 dependabot-script が Github Pakages への参照ができないため、 fork して少し修正をしています。

dependabot-core はプライベートレジストリの認証に対応しているので、 scriptを少し修正して対応しています。

なお、 dependabot-script にも同様の対応のPR Read private repository credentials from environment が出ているので、いずれ公式に対応されるかもしれないです。

とくに GitHub Packages みたいなプライベートレジストリを使っていないのであれば、公式の dependabot-script を使ってもらえば良いと思います。