🙆

CodeCommitトリガーでPRの差分ファイルを取得する

2024/04/22に公開

記事の内容

普段Githubを使っていると、なかなかCodeCommitを使う機会がないのですが、たまたまCodeCommitを使う機会がありました。
プルリクエスト作成時に、コードの差分はAWSコンソールから確認できますが、差分をS3にバックアップしたい要件がありました。gitコマンドでなくaws-cliでできそうで、cliでできるというこはaws-sdkでも大体できるので、CodeCommitトリガーでLambdaを実行することでトライしてみました。

関係する技術・ツール

  • AWS CodeCommit
  • Amazon EventBridge
  • AWS Lambda(Node.js 20)
  • AWS CDK
  • typescript

作業の流れ

  1. Lambda作成
  2. EventBridgeの作成
  3. 動作確認

1. Lambda作成

いきなりですが、lambdaのコードです。
CodeCommitでトリガーでEventBridgeを実行することでLambdaを実行します。
今回はCodeCommit上でプルリクエストの作成または、プルリクエストブランチが更新された場合に差分を取得してみます。
この場合EventBridgeからLambdaに渡される引数には下記のようなペイロードになります。

https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/monitoring-events.html#pullRequestCreated

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import {
  CodeCommitClient,
  GetDifferencesCommand,
  GetDifferencesCommandOutput,
  GetFileCommand,
} from "@aws-sdk/client-codecommit";
import type { EventBridgeHandler } from "aws-lambda";
import type { CodeCommitPullRequestStateChangeEventDetail } from "./sample-event";
import { parse, join } from "path";
import { DateTime } from "luxon";

/**
 * 差分ファイルのみS3にアップロードするサンプル
 */
export const handler: EventBridgeHandler<
  "CodeCommit Pull Request State Change",
  CodeCommitPullRequestStateChangeEventDetail,
  void
> = async (event) => {
  let nextToken;
  const today = DateTime.now().toFormat("yyyyMMdd");

  for await (const { differences, NextToken } of getDiffFile(
    event.detail,
    nextToken,
  )) {
    if (!differences) {
      return;
    }

    {
      for (const { changeType, afterBlob } of differences) {
        if (["M", "A"].includes(changeType!)) {
          const result = await getFile(event.detail, afterBlob!.path!);
          console.log(result);

          const { base } = parse(event.detail.sourceReference);
          const baseKey = join(today, base);
          const key = join(baseKey, result.filePath!);

          await putDiffFile(key, result.fileContent!);
        }
      }
      nextToken = NextToken;
    }
    while (nextToken);
  }
  return;
};

const getCodeCommitClient = () => {
  return new CodeCommitClient();
};

/**
 * 差分はファイルベースで取得される
 * つまり1ファイルを何回更新していようと、1ファイルにつき1件の差分が取得される
 * afterCommitSpecifier: 最新コミット
 * beforeCommitSpecifier: 比較対象のコミット(派生元のコミット)
 * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codecommit/command/GetDifferencesCommand/
 */
async function* getDiffFile(
  {
    repositoryNames: [repositoryName],
    sourceCommit,
    destinationCommit,
  }: CodeCommitPullRequestStateChangeEventDetail,
  nextToken?: string,
): AsyncGenerator<GetDifferencesCommandOutput> {
  const codeCommitClient = getCodeCommitClient();

  yield codeCommitClient.send(
    new GetDifferencesCommand({
      repositoryName,
      afterCommitSpecifier: sourceCommit,
      beforeCommitSpecifier: destinationCommit,
      NextToken: nextToken,
    }),
  );
}

/**
 * 差分で抽出されたファイルを取得する。
 * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codecommit/command/GetFileCommand/
 */
const getFile = async (
  {
    repositoryNames: [repositoryName],
    sourceReference,
  }: CodeCommitPullRequestStateChangeEventDetail,
  filePath: string,
) => {
  const codeCommitClient = getCodeCommitClient();

  return codeCommitClient.send(
    new GetFileCommand({
      repositoryName,
      filePath,
      commitSpecifier: sourceReference,
    }),
  );
};

const getS3Client = () => {
  return new S3Client();
};

const putDiffFile = async (Key: string, Body: Uint8Array) => {
  const s3Client = getS3Client();

  return s3Client.send(
    new PutObjectCommand({
      Bucket: process.env.BUCKET_NAME,
      Key,
      Body,
    }),
  );
};

非nullアサーション多用していて乱暴ですが、雰囲気としてはこんな感じになると思います。
sdkの型的にはGetDifferencesCommandOutPutのプロパティはオプションらしいのですが、どういう場合にundefinedなのかは未検証で不明です。

2. EventBridgeの作成

次にCodeCommitトリガーをフィルタリングするためのEventBridgeを作成します。
AWS CDkのコードです。EventBridge以外の関係リソースも含まれています。

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as codecommit from "aws-cdk-lib/aws-codecommit";
import * as lambda from "aws-cdk-lib/aws-lambda-nodejs";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import * as events from "aws-cdk-lib/aws-events";
import * as event_targets from "aws-cdk-lib/aws-events-targets";

export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const repo = new codecommit.Repository(this, "Repo", {
      repositoryName: "demo-repo",
    });

    const bucket = new s3.Bucket(this, "Bucket");

    const lambdaRole = new iam.Role(this, "CodeCommitTriggerRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AWSLambdaBasicExecutionRole",
        ),
      ],
      inlinePolicies: {
        CodeCommitTriggerPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ["s3:PutObject"],
              resources: [bucket.bucketArn + "/*"],
            }),
            new iam.PolicyStatement({
              actions: ["codecommit:Get*"],
              resources: [repo.repositoryArn],
            }),
          ],
        }),
      },
    });

    const codeCommitDiffLambda = new lambda.NodejsFunction(
      this,
      "CodeCommitTriggerHandler",
      {
        entry: "functions/codecommit-trigger/index.ts",
        handler: "handler",
        runtime: Runtime.NODEJS_20_X,
        timeout: cdk.Duration.seconds(30),
        environment: {
          BUCKET_NAME: bucket.bucketName,
        },
        role: lambdaRole,
        bundling: {
          sourceMap: true,
          tsconfig: "./tsconfig.json",
        },
      },
    );

    /**
     * PR作成時またはPRブランチの更新時かつ
     * ブランチ名のプレフィックスがreleaseである場合にトリガされる
     */
    new events.Rule(this, "CodeCommitTriggerRule", {
      eventPattern: {
        source: ["aws.codecommit"],
        detailType: ["CodeCommit Pull Request State Change"],
        resources: [repo.repositoryArn],
        detail: {
          event: ["pullRequestCreated", "pullRequestSourceBranchUpdated"],
          sourceReference: events.Match.prefix("refs/heads/release"),
        },
      },
      targets: [new event_targets.LambdaFunction(codeCommitDiffLambda)],
    });
  }
}

ポイントとしてはRuleのeventPatternです。
上記のようにすることで、 PR作成時またはPRブランチの更新時かつブランチ名のプレフィックスがreleaseである場合にLambdaを実行できます。

3. 動作確認

各リソースの作成ができたら、作成したCodeCommitに名前がreleaseで始まるブランチを作成してコミットします。そしてプルリクエストを作成してみましょう。
するとS3バケットに年月日/ブランチ名/リポジトリのファイルのパスのような感じでファイルがS3にアップロードされます。
作成したプルリクえすがクローズされるまでトリガーは有効で、2回目以降さらにブランチ更新した際も動作します。これでソースの更新と連動して常に最新のコードがバックアップされます。

以上となります。
本日はCodeCommitトリガーを活用したコードのバックアップをトライしてみました。
トリガーの種類は他にも多々あるので、これを活用すれば様々なイベントでトリガーができそうですね。

https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/monitoring-events.html

NCDCエンジニアブログ

Discussion