はじめに
4月から 個人開発 をはじめて早1ヵ月。
AIコーディングエージェントによる実装を経て、「中身が全くわからない。これではまずい。」と思い手作業でのコーディングへ変更しました。
時間は掛かってますがそのお陰で実務では触らない環境へ慣れてきました。
でも日々、「CI/CD 触ってみたいな。」という思いがあったので、 なら個人開発でやったらいいじゃん! と思い試してみました。
構成のイメージは下記になります。
.
├── .github/workflows/main.yml
├── docker-compose.yml
└── frontend/ <--- CIのworking-directory
├── .eslintrc.cjs
├── package.json
└── src/
Gemini に聞いてみると「ルートディレクトリに.github\workflowsフォルダを作成してmain.ymlを配置するだけ」と教えてもらったので早速試してみました!
早とちりコミット
この手軽さ故に、 main.yml を作成し lint をインストール。
packedge.json への反映も済んだのですぐにコミットしました。
すると lintスクリプトが見当たらない というエラーが出ました。
Gemini の「ルートディレクトリに.github\workflowsフォルダを作成してmain.ymlを配置するだけ」
という説明を鵜呑みにしてしまいました。
原因はローカルからコンテナ内へファイル連携している層が1つずれていた為でした。
.eslintrc.cjs を frontend 配下へ移動する事により解決しました。
frontend:
build: ./frontend
depends_on:
- backend
ports:
- "5173:5173"
volumes:
- ./frontend:/app # ここが原因
Docker を使っていると陥りがちの罠でした。
AIコーディングエージェントに頼らないからこそ理解できました。
ローカル(Docker環境)では動くけど、CI(GitHub Actions環境)ではパスが違っていて動かなかった
際のトラブルシューティングの1つになればと思います。
ローカルでのエラー解決
コミットが早かったので初歩的なエラーで止まってしまいました。
なので今度はローカルできちんと動かしてみます。
すると2種類のエラーが出てきました。
- 使われていない変数がある
- パースができない
PS C:\dev\ws\docker\Vi-va-Link-detail> docker compose run --rm frontend npm run lint
[+] 2/2t 2/22
✔ Container vi-va-link-detail-db-1 Healthy 0.5s
✔ Container vi-va-link-detail-backend-1 Running 0.0s
Container vi-va-link-detail-frontend-run-f1cf6394605d Creating
Container vi-va-link-detail-frontend-run-f1cf6394605d Created
> bus-logistics-frontend@0.0.1 lint
> eslint . --ext .vue,.js --fix
/app/src/router/index.js
29:24 error '_from' is defined but never used no-unused-vars
/app/src/views/ScheduleCreate.vue
249:7 error '_debounceTimers' is assigned a value but never used no-unused-vars
/app/src/views/ScheduleSearch.vue
355:10 error '_toBBox' is defined but never used no-unused-vars
/app/src/views/__tests__/BookingList.test.js
124:30 error Parsing error: Identifier directly after number
/app/src/views/__tests__/Tracking.test.js
110:30 error Parsing error: Identifier directly after number
✖ 5 problems (5 errors, 0 warnings)
まず最初の 「使われていない変数がある」 ですが、
ruleを作ってそこでアンダーバーが先頭にある変数のエラーを回避しました。
module.exports = {
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
],
env: {
browser: true,
node: true,
es2020: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'no-unused-vars': ['error', {
// 引数や変数の名前がアンダースコア(_)で始まる場合は、
// 「定義されているが未使用」であってもエラーにしない
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_'
}],
}
}
もう1つの 「パースができない」 ですが、
テストソース内に vi.advanceTimersByTime(30_000) という表記があったのでアンダーバーを取る事によりこちらも解決しました。
修正前
it('照会後30秒ごとに自動更新する', async () => {
vi.mocked(axios.get).mockResolvedValue({
data: {
tracking_number: 'TRK-POLL',
status: 'accepted',
status_updated_at: '2099-01-01T00:00:00Z',
schedule: { origin_name: 'A', dest_name: 'B', depart_at: '2099-01-01T00:00:00Z' },
},
})
const wrapper = mountWrapper()
await wrapper.find('input[type="text"]').setValue('TRK-POLL')
await wrapper.find('form').trigger('submit')
await vi.waitFor(() => expect(axios.get).toHaveBeenCalledTimes(1))
vi.advanceTimersByTime(30_000) // <-----ここが悪さをしている
await vi.waitFor(() => expect(axios.get).toHaveBeenCalledTimes(2))
})
ESLintのパース設定(ECMAScriptのバージョン)が古いこと によるエラー検知みたいです。
無くても挙動は変わらないみたいなので今回は一旦削除で対応しました。
修正後
it('照会後30秒ごとに自動更新する', async () => {
vi.mocked(axios.get).mockResolvedValue({
data: {
tracking_number: 'TRK-POLL',
status: 'accepted',
status_updated_at: '2099-01-01T00:00:00Z',
schedule: { origin_name: 'A', dest_name: 'B', depart_at: '2099-01-01T00:00:00Z' },
},
})
const wrapper = mountWrapper()
await wrapper.find('input[type="text"]').setValue('TRK-POLL')
await wrapper.find('form').trigger('submit')
await vi.waitFor(() => expect(axios.get).toHaveBeenCalledTimes(1))
vi.advanceTimersByTime(30000) // <-----意味は同じなのでこの修正でエラー回避
await vi.waitFor(() => expect(axios.get).toHaveBeenCalledTimes(2))
})
今度はビルドエラー発生
ローカルでのエラー解消を経てコミットしたんですが、
今度はビルドエラーが出てしまいました。
src/router/index.js の構文ミスです。
修正前
const routes = [
{ path: '/shipper/companies', name: 'CompanyList', component: () => import('../views/CompanyList.vue'), meta: { requiresAuth: true, role: 'shipper' } },
{ path: '/tracking', name: 'Tracking', component: () => import('../views/TrackingView.vue/index.js') }, // <-----ここがエラーの原因
...
]
import('../views/TrackingView.vue/index.js')
この行に余分なindex.jsが付いているので削除したら解決しました。
↓
import('../views/TrackingView.vue')
修正後
const routes = [
{ path: '/shipper/companies', name: 'CompanyList', component: () => import('../views/CompanyList.vue'), meta: { requiresAuth: true, role: 'shipper' } },
{ path: '/tracking', name: 'Tracking', component: () => import('../views/TrackingView.vue') },
...
]
CIの実行正常終了!
トライアンドエラー を繰り返してようやく正常終了までたどり着きました!
この画面見ると達成感を感じました!開発をしていると実感します。
ちなみに脆弱性スキャンも同時に行っているので内容確認してみました。
frontend-ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
steps:
... # 省略
- name: Security Audit
run: npm audit --audit-level=high # <-----この部分
高リスクの脆弱性はない という結果になりました。
ここまで自動でやってくれると助かりますね!
おわりに
今回の学びまとめ
- パスの差異: ローカ ルDocker の
volumes設定と、CI環境でのカレントディレクトリの違いに注意が必要。 - Lintのパースエラー:
30_000のような最新のJS記法を使うなら、ESLintのecmaVersion設定を確認すること。 - AIとの付き合い方: AIの「フォルダを作って置くだけ」という言葉には、プロジェクト固有の事情(ディレクトリ構造など)が含まれていないことを意識する。
個人開発 では実務でできない事にあえて挑戦しています。
その分 トライアンドエラー が多くつまずきそうになりますが、AIコーディングエージェントが復旧したからこそこの泥臭さが今後役に立つはず!
保守のできないエンジニアにはなりたくないので、今後も時間をかけて粘り強く色んな事に挑戦していきます!



