GitHub Actions CI/CD パイプライン構築の苦闘記 - TypeScript移行からFirebase統合まで

はじめに

Astroベースのブログプロジェクトを完全TypeScript化し、GitHub ActionsでCI/CDパイプラインを構築する過程で、予想以上に多くの課題に直面しました。最終的にデプロイ成功までに約50回のコミットが必要でした。この記事では、遭遇したエラーとその解決方法を時系列で詳細に記録します。

プロジェクトの初期状態

  • フレームワーク: Astro 5.2.5
  • 言語: JavaScript/TypeScript混在
  • CI/CD: GitHub Actions (基本的なビルド&デプロイのみ)
  • 品質管理ツール: 未設定

Phase 1: TypeScript完全移行

実施内容

すべてのJavaScriptファイルをTypeScriptに移行:

Terminal window
# 移行したファイル
src/utils/*.js *.ts
src/config.js config.ts
astro.config.mjs astro.config.ts

遭遇した問題1: astro:content モジュールエラー

// エラー: Cannot find module 'astro:content' or its corresponding type declarations
import { getCollection } from 'astro:content'

解決方法: src/env.d.ts を作成し、型定義を追加

src/env.d.ts
/// <reference types="astro/client" />
declare module 'astro:content' {
export type CollectionEntry<C extends keyof typeof collections> = {
id: string
slug: string
body: string
collection: C
data: InferEntrySchema<C>
render(): Promise<{
Content: any
headings: any[]
remarkPluginFrontmatter: Record<string, any>
}>
}
type InferEntrySchema<C extends keyof typeof collections> = C extends 'posts'
? {
title: string
description: string
pubDate: Date
updatedDate?: Date
heroImage?: string
category?: string
tags: string[]
draft: boolean
}
: never
// 他の必要な型定義...
}

遭遇した問題2: 型推論エラー

// エラー: Type 'unknown[]' is not assignable to type 'string[]'
const tags = [...new Set(posts.flatMap((post) => post.data.tags || []))]

解決方法: 明示的な型注釈を追加

const allTags: string[] = posts.flatMap((post: CollectionEntry<'posts'>) => {
const postTags: string[] = post.data.tags || []
return postTags
})
const tags: string[] = [...new Set(allTags)]

Phase 2: ESLint v9 移行

遭遇した問題: 設定ファイル形式エラー

ESLint couldn't find an eslint.config.(js|mjs|cjs) file

ESLint v9では従来の .eslintrc.json が使えなくなりました。

解決方法: フラットコンフィグ形式に移行

eslint.config.js
import js from '@eslint/js'
import typescriptParser from '@typescript-eslint/parser'
import typescriptPlugin from '@typescript-eslint/eslint-plugin'
import astroPlugin from 'eslint-plugin-astro'
import reactPlugin from 'eslint-plugin-react'
import globals from 'globals'
export default [
js.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
parser: typescriptParser,
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
'@typescript-eslint': typescriptPlugin,
react: reactPlugin,
},
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-unused-vars': 'off',
},
},
{
files: ['**/*.astro'],
plugins: {
astro: astroPlugin,
},
processor: 'astro/astro',
},
]

Phase 3: Prettier フォーマット問題

遭遇した問題: Markdownファイルのパースエラー

[error] src/content/posts/fixing-theme-switching-issues.md: SyntaxError: Unexpected character

コードブロック内の特殊文字が原因でPrettierがパースに失敗。

解決方法: .prettierignore に問題のあるファイルを追加

.prettierignore
src/content/posts/fixing-theme-switching-issues.md
src/content/posts/tailwind-troubleshooting.md
src/pages/archive.astro

Phase 4: Firebase 条件付き初期化

遭遇した問題1: CI環境でのFirebaseエラー

Firebase: Error (auth/invalid-api-key)

環境変数が設定されていないCI環境でFirebase初期化が失敗。

解決方法: 条件付き初期化の実装

src/lib/firebase.ts
const hasFirebaseConfig = !!(
import.meta.env.PUBLIC_FIREBASE_API_KEY &&
import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN &&
import.meta.env.PUBLIC_FIREBASE_PROJECT_ID
)
let app: FirebaseApp | null = null
let auth: Auth | null = null
let googleProvider: GoogleAuthProvider | null = null
if (hasFirebaseConfig) {
try {
const firebaseConfig = {
apiKey: import.meta.env.PUBLIC_FIREBASE_API_KEY,
authDomain: import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.PUBLIC_FIREBASE_PROJECT_ID,
// ...
}
app = initializeApp(firebaseConfig)
auth = getAuth(app)
googleProvider = new GoogleAuthProvider()
} catch (error) {
console.warn('Firebase initialization failed:', error)
app = null
auth = null
googleProvider = null
}
}
export { auth, googleProvider, hasFirebaseConfig }

遭遇した問題2: TypeScript 型エラー

// エラー: Argument of type 'Auth | null' is not assignable to parameter of type 'Auth'
await signInWithPopup(auth, googleProvider)

解決方法: null チェックの追加

const handleLogin = async () => {
if (!auth || !googleProvider) return
try {
await signInWithPopup(auth, googleProvider)
} catch (error) {
console.error('Error signing in with Google', error)
}
}

遭遇した問題3: SSRビルドエラー

Unable to render LoginButton!
Does not conditionally return `null` or `undefined` when rendered on the server.

サーバーサイドレンダリング時にコンポーネントが null を返すことによるエラー。

解決方法: client:only ディレクティブの使用

<!-- src/components/Header.astro --><!-- 変更前 -->
<LoginButton client:load />
<!-- 変更後 -->
<LoginButton client:only="react" />

Phase 5: CI/CD パイプライン最適化

最終的なGitHub Actions設定

name: Deploy to GitHub Pages
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.2.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check
run: pnpm run type-check
- name: Lint check
run: pnpm run lint
- name: Format check
run: pnpm run format:check
- name: Build test
run: pnpm run build
build:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: quality
runs-on: ubuntu-latest
steps:
- name: Checkout your repository using git
uses: actions/checkout@v4
- name: Install, build, and upload your site
uses: withastro/action@v3
with:
package-manager: pnpm@10.2.1
deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

重要な学び

1. ローカルテストの重要性

Terminal window
# プッシュ前に必ず実行
pnpm run type-check
pnpm run lint
pnpm run format:check
pnpm run build

2. 段階的な移行

一度にすべてを変更するのではなく、以下の順序で段階的に実施:

  1. TypeScript移行
  2. 型エラー修正
  3. ESLint設定
  4. Prettier設定
  5. CI/CD統合

3. エラーメッセージの詳細な読み取り

特にTypeScriptのエラーは、エラーメッセージを丁寧に読むことで解決の糸口が見つかります。

4. 環境差異への対応

開発環境とCI環境の違いを考慮した実装が必要:

  • 環境変数の有無
  • OSの違い(Windows vs Linux)
  • Node.jsバージョン

統計データ

  • 総コミット数: 約50回
  • 修正にかかった時間: 約8時間
  • 修正したファイル数: 30以上
  • 解決したエラー種別: 7種類

まとめ

CI/CDパイプラインの構築は、単にツールを設定するだけでなく、プロジェクト全体の品質向上につながる重要なプロセスです。多くのエラーに遭遇しましたが、それぞれが貴重な学習機会となりました。

特に重要なのは:

  1. エラーを恐れない - エラーは学習の機会
  2. 段階的に進める - 一度にすべてを変更しない
  3. ローカルで確認 - プッシュ前の確認を徹底
  4. ドキュメントを残す - 後で同じ問題に遭遇した時のために

このプロジェクトを通じて、堅牢なCI/CDパイプラインの価値を改めて実感しました。初期設定は大変ですが、長期的には開発効率と品質の大幅な向上につながります。

参考リンク