Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

TypeScriptでGitHub Actionsカスタムアクション開発完全ガイド!CI/CDワークフローを効率化する実践的な作り方

記事のサムネイル
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

GitHub Actionsカスタムアクションとは

GitHub Actionsのカスタムアクションは、繰り返し使用されるタスクを再利用可能なコンポーネントとして作成できる機能です。通常のワークフローファイルではステップごとに処理を記述しますが、カスタムアクションを使用することで複雑な処理をひとつのアクションにまとめることができます。

カスタムアクションには主に3つのタイプがあります:

# Docker コンテナアクション
runs:
  using: 'docker'
  image: 'Dockerfile'

# JavaScript アクション
runs:
  using: 'node20'
  main: 'dist/index.js'

# 複合アクション
runs:
  using: 'composite'
  steps:
    - run: echo "Hello World"
      shell: bash

特にJavaScriptアクション(TypeScriptで開発)は、高速に実行でき、GitHubのAPIとの連携が簡単で、デバッグしやすいため最も人気があります。開発したカスタムアクションは、同じリポジトリ内だけでなく、他のリポジトリからも使用できるため、チーム全体の開発効率を大幅に向上させることができます。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

TypeScriptでカスタムアクション開発のメリット

TypeScriptでGitHub Actionsのカスタムアクションを開発することには、多くのメリットがあります:

型安全性と開発効率の向上

interface ActionInputs {
  token: string;
  repository: string;
  branch?: string;
}

// 型チェックによりランタイムエラーを事前に防げます
function validateInputs(inputs: ActionInputs): boolean {
  return inputs.token.length > 0 && inputs.repository.includes('/');
}

豊富なライブラリエコシステム GitHub公式が提供する@actions/core@actions/githubといったライブラリを活用でき、複雑なAPI呼び出しやファイル操作を簡単に実装できます。

優れたデバッグ環境 Visual Studio Codeなどのエディタでブレークポイントを設定し、ローカル環境でステップ実行によるデバッグが可能です。これにより、GitHubにpushする前に問題を発見・修正できます。

保守性の高いコード TypeScriptの静的型チェックとコードの可読性により、長期的なメンテナンスが容易になります。また、リファクタリング時の安全性も大幅に向上します。

あわせて読みたい

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

開発環境のセットアップ手順

TypeScriptでGitHub Actionsカスタムアクションを開発するための環境をセットアップしましょう。

必要なツールのインストール

# Node.js(推奨:LTS版)
# TypeScript
npm install -g typescript

# GitHub Actions用のライブラリ
npm init -y
npm install @actions/core @actions/github
npm install -D @types/node typescript ts-node @vercel/ncc

プロジェクト構造の作成

my-custom-action/
├── src/
│   └── main.ts
├── dist/
│   └── index.js
├── action.yml
├── package.json
└── tsconfig.json

TypeScript設定ファイル(tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

package.jsonの追加設定

{
  "scripts": {
    "build": "tsc",
    "package": "ncc build dist/main.js --out dist",
    "dev": "npm run build && npm run package"
  }
}

@vercel/nccは、Node.jsアプリケーションを単一ファイルにバンドルするツールで、GitHub Actionsで使用する際に依存関係を含めてパッケージ化するために必要です。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

基本的なカスタムアクションの実装例

実際にPull Requestにラベルを自動追加するカスタムアクションを作成してみましょう。

action.yml の定義

name: 'Auto Label PR'
description: 'Automatically adds labels to Pull Requests based on file changes'
inputs:
  github-token:
    description: 'GitHub token for API access'
    required: true
  label-rules:
    description: 'JSON string of label rules'
    required: true
    default: '{"src/": "source-code", "docs/": "documentation"}'
outputs:
  added-labels:
    description: 'Labels that were added to the PR'
runs:
  using: 'node20'
  main: 'dist/index.js'

メインロジック(src/main.ts)

import * as core from '@actions/core';
import * as github from '@actions/github';

interface LabelRules {
  [path: string]: string;
}

async function run(): Promise<void> {
  try {
    // 入力パラメータの取得
    const token = core.getInput('github-token', { required: true });
    const labelRulesInput = core.getInput('label-rules', { required: true });
    const labelRules: LabelRules = JSON.parse(labelRulesInput);

    // GitHub API クライアントの初期化
    const octokit = github.getOctokit(token);
    const context = github.context;

    // Pull Request の変更ファイルを取得
    const { data: files } = await octokit.rest.pulls.listFiles({
      owner: context.repo.owner,
      repo: context.repo.repo,
      pull_number: context.payload.pull_request?.number || 0,
    });

    // ラベルの決定
    const labelsToAdd = new Set<string>();
    files.forEach(file => {
      Object.keys(labelRules).forEach(path => {
        if (file.filename.startsWith(path)) {
          labelsToAdd.add(labelRules[path]);
        }
      });
    });

    // ラベルの追加
    if (labelsToAdd.size > 0) {
      await octokit.rest.issues.addLabels({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.payload.pull_request?.number || 0,
        labels: Array.from(labelsToAdd),
      });

      core.setOutput('added-labels', Array.from(labelsToAdd).join(','));
      core.info(`Added labels: ${Array.from(labelsToAdd).join(', ')}`);
    } else {
      core.info('No labels to add based on the current rules');
    }
  } catch (error) {
    core.setFailed(`Action failed: ${error}`);
  }
}

run();

ワークフローでの使用例

name: Auto Label PR
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - uses: your-username/auto-label-pr@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          label-rules: |
            {
              "src/": "source-code",
              "docs/": "documentation",
              "test/": "testing"
            }

このカスタムアクションは、Pull Requestの変更ファイルのパスに基づいて、自動的に適切なラベルを追加します。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

関連記事

実践的なカスタムアクション開発のテクニック

プロダクションレベルのカスタムアクションを開発するための実践的なテクニックをご紹介します。

エラーハンドリングとロギング

import * as core from '@actions/core';

class ActionError extends Error {
  constructor(message: string, public code: string) {
    super(message);
    this.name = 'ActionError';
  }
}

async function safeApiCall<T>(
  apiCall: () => Promise<T>,
  errorMessage: string
): Promise<T> {
  try {
    return await apiCall();
  } catch (error) {
    core.error(`${errorMessage}: ${error}`);
    throw new ActionError(errorMessage, 'API_ERROR');
  }
}

// 使用例
const files = await safeApiCall(
  () => octokit.rest.pulls.listFiles(params),
  'Failed to fetch PR files'
);

設定の外部化とバリデーション

interface ActionConfig {
  readonly githubToken: string;
  readonly maxLabels: number;
  readonly dryRun: boolean;
}

function getConfig(): ActionConfig {
  const config = {
    githubToken: core.getInput('github-token', { required: true }),
    maxLabels: parseInt(core.getInput('max-labels') || '5'),
    dryRun: core.getBooleanInput('dry-run'),
  };

  // バリデーション
  if (config.maxLabels < 1 || config.maxLabels > 20) {
    throw new ActionError('max-labels must be between 1 and 20', 'INVALID_CONFIG');
  }

  return config;
}

パフォーマンス最適化

// 並列処理でAPIコールを最適化
async function processFiles(files: any[], labelRules: LabelRules) {
  const batchSize = 10;
  const results = [];

  for (let i = 0; i < files.length; i += batchSize) {
    const batch = files.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(file => analyzeFileForLabels(file, labelRules))
    );
    results.push(...batchResults);
  }

  return results;
}

再利用可能なユーティリティ関数

// utils/github.ts
export class GitHubUtils {
  constructor(private octokit: any, private context: any) {}

  async getCurrentPR() {
    if (!this.context.payload.pull_request) {
      throw new ActionError('This action can only run on pull requests', 'INVALID_CONTEXT');
    }
    return this.context.payload.pull_request;
  }

  async addLabelsIfNotExist(labels: string[]) {
    const existingLabels = await this.getExistingLabels();
    const newLabels = labels.filter(label => !existingLabels.includes(label));
    
    if (newLabels.length > 0) {
      return this.addLabels(newLabels);
    }
    return [];
  }
}

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

テストとデバッグの効率的な方法

カスタムアクションの品質を保つためには、適切なテストとデバッグが不可欠です。

ユニットテストの実装

// tests/main.test.ts
import * as core from '@actions/core';
import { run } from '../src/main';

// GitHub Actionsのモック
jest.mock('@actions/core');
jest.mock('@actions/github');

const mockCore = core as jest.Mocked<typeof core>;

describe('Auto Label PR Action', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('should add labels based on file changes', async () => {
    // モックデータの設定
    mockCore.getInput.mockImplementation((name: string) => {
      switch (name) {
        case 'github-token': return 'fake-token';
        case 'label-rules': return '{"src/": "source-code"}';
        default: return '';
      }
    });

    // テスト実行
    await run();

    // アサーション
    expect(mockCore.setOutput).toHaveBeenCalledWith('added-labels', 'source-code');
  });
});

ローカルデバッグの設定

// scripts/debug.ts
import * as core from '@actions/core';
import { run } from '../src/main';

// ローカルテスト用の環境変数設定
process.env.INPUT_GITHUB_TOKEN = 'your-test-token';
process.env.INPUT_LABEL_RULES = JSON.stringify({
  'src/': 'source-code',
  'docs/': 'documentation'
});

// GitHub Contextのモック
const mockContext = {
  repo: { owner: 'test-owner', repo: 'test-repo' },
  payload: {
    pull_request: { number: 123 }
  }
};

// デバッグ実行
run().catch(error => {
  console.error('Debug failed:', error);
});

実際のワークフローでのテスト

# .github/workflows/test-action.yml
name: Test Custom Action
on:
  pull_request:
    paths:
      - 'action.yml'
      - 'src/**'
      - 'dist/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Test action
        uses: ./
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          label-rules: |
            {
              "src/": "test-source",
              "docs/": "test-docs"
            }

ログとデバッグ出力の活用

// デバッグレベルのログ出力
core.debug(`Processing ${files.length} files`);
core.info(`Labels to add: ${labelsToAdd.join(', ')}`);
core.warning('No matching files found for label rules');

// 条件付きデバッグ出力
if (core.isDebug()) {
  core.debug(`Full file list: ${JSON.stringify(files, null, 2)}`);
}

適切なテストとデバッグ環境を整備することで、信頼性の高いカスタムアクションを効率的に開発できます。このガイドを参考に、あなたのCI/CDワークフローを効率化するカスタムアクションを作成してみてください。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ