ワーカーを、時計を待たずに起こせるようにする #329
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # push / PR でテストを実行し、main への push で Docker イメージをビルドして GHCR へ公開する。 | |
| # ghcr.io/<owner>/chiezo-app … 配信 API(app/Dockerfile) | |
| # ghcr.io/<owner>/chiezo-ingest … 取り込みバッチ + chiezo-trigger(ingest/Dockerfile) | |
| # ghcr.io/<owner>/chiezo-bridge … CLI ブリッジ(bridge/Dockerfile)。Claude Code / Codex CLI を | |
| # OpenAI 互換の口に見せる。既定では立たず、使う人だけが pull する | |
| # | |
| # テストはフィクスチャ完結(ネットワーク・実データ不要)なので、そのまま CI で回る。 | |
| # イメージ公開はダイジェストマージ方式: | |
| # アーキごとに「そのアーキのネイティブランナー」で並列ビルドし、ダイジェストで | |
| # push したものを最後の merge ジョブでマニフェスト(latest / sha-xxxxxxx)にまとめる。 | |
| # QEMU エミュレーションだとビルドが極端に遅く不安定なため。 | |
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| # 週 1 の定期実行(月曜 6:00 JST)。実際に入る版は requirements.txt(ロック)で | |
| # 固定しているが、requirements.in の範囲指定(>=)の中では上流が動き続けている。 | |
| # test-latest ジョブが最新の依存で回して、上流の破壊的変更(実例: mcp 2.0 が | |
| # mcp.server.fastmcp を削除し import から落ちた)にコミットを待たず気づく。 | |
| # 検知が目的なので、下の build/merge(イメージ公開)は定期実行では走らせない。 | |
| schedule: | |
| - cron: "0 21 * * 0" | |
| # 同じ参照の実行を直列にする。**公開は 2 段階**(build がタグ無しのダイジェストで | |
| # push し、merge がそれにタグを付ける)なので、その途中に別の実行の掃除が重なると、 | |
| # まだタグの無いイメージを「タグ無しの版」として消される —— 実測で、push を 2 つ | |
| # 続けたときに後の実行が merge で `not found` になって公開だけが落ちた | |
| # (build は成功しているので、失敗を見るまで古いイメージが動き続ける)。 | |
| # | |
| # 打ち切らない。**公開の途中で止めると、タグの付いていないイメージが残る**。 | |
| # 参照ごとの群なので、待たされるのは同じブランチへ続けて push したときだけ。 | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: false | |
| # 既定はワークフロー全体で読み取りのみ。GHCR への push 権限は、それを実際に使う | |
| # build / merge ジョブだけに与える(public リポジトリは fork からの PR が来る前提で、 | |
| # 権限は使う場所まで絞っておく)。 | |
| permissions: | |
| contents: read | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| # ruff の版は requirements-dev.txt に固定してある(版が変わると指摘が増減するので、 | |
| # 手元と CI で同じものを使う)。ハッシュ付きロックは丸ごと入れるのが素直。 | |
| - run: pip install -r requirements-dev.txt | |
| - run: ruff check . | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| # 固定した版で回す(requirements-dev.txt は requirements-dev.in をコンパイルしたもの)。 | |
| - run: pip install -r requirements-dev.txt | |
| - run: python -m pytest -v | |
| # 上限を外して最新の依存で回す canary。ロックを入れたことで通常のテストは固定版に | |
| # なったため、上流の破壊的変更に気づく役目をこちらへ移した(週 1 の定期実行と手動実行のみ)。 | |
| # 失敗しても公開は止めない(main への push では動かないので needs にも入れない)。 | |
| test-latest: | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| - run: pip install -r requirements-dev.in | |
| - run: pip list | |
| - run: python -m pytest -v | |
| # アーキごとにネイティブランナーでビルドし、タグを付けずダイジェストで push する。 | |
| # 無料の ubuntu-24.04-arm ランナーは public リポジトリ限定のため、private の間は | |
| # 公開ジョブをスキップする(public 化後の main への push から動き出す)。 | |
| build: | |
| # 公開するのは main への push と手動実行のみ(PR はテストだけ、定期実行は検知だけ)。 | |
| if: >- | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| && github.event.repository.private == false | |
| needs: [lint, test] | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image: [app, ingest, bridge] | |
| arch: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| exclude: | |
| # ブリッジは amd64 だけにする。中身が CLI 3 本(圧縮後 300MB)で、arm64 を作ると | |
| # 保存量がそのまま倍になる。**同梱の CLI は amd64 のバイナリを取り出しているので、 | |
| # arm64 を作るなら Dockerfile 側の取り出し先も変える必要がある**(いまは | |
| # codex の vendor パスに x86_64 が直書きしてある)。 | |
| - image: bridge | |
| arch: | |
| platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| runs-on: ${{ matrix.arch.runner }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: docker/setup-buildx-action@v4 | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ghcr.io/${{ github.repository }}-${{ matrix.image }} | |
| # ビルド日時。イメージに焼いて画面に出す(タグでは判別できないため) | |
| - id: buildtime | |
| run: echo "value=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| - id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| # app だけリポジトリ全体を文脈にする。やること画面(tasks-frontend/)を | |
| # 同じイメージへ焼き、公開する面は compose の command だけで分けるため。 | |
| # 送る量はルートの .dockerignore で絞ってある(data/ は数十 GB ある) | |
| context: ${{ matrix.image == 'app' && '.' || format('./{0}', matrix.image) }} | |
| file: ./${{ matrix.image }}/Dockerfile | |
| platforms: ${{ matrix.arch.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| # 動いているイメージがどのコミットかを画面に出すために焼き込む | |
| build-args: | | |
| BUILD_SHA=${{ github.sha }} | |
| BUILD_TIME=${{ steps.buildtime.outputs.value }} | |
| # タグではなくダイジェストで push する(マニフェストは merge ジョブで作る) | |
| outputs: type=image,name=ghcr.io/${{ github.repository }}-${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=${{ matrix.image }}-${{ matrix.arch.platform }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.image }}-${{ matrix.arch.platform }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-${{ matrix.image }}-${{ strategy.job-index }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # 各アーキのダイジェストをイメージごとに 1 つのマニフェスト(latest / sha-xxxxxxx)にまとめて push する | |
| merge: | |
| needs: [build] | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| image: [app, ingest, bridge] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: digests-${{ matrix.image }}-* | |
| merge-multiple: true | |
| - uses: docker/setup-buildx-action@v4 | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ghcr.io/${{ github.repository }}-${{ matrix.image }} | |
| # latest(既定ブランチのみ)+ コミット SHA(sha-xxxxxxx)の 2 タグ。 | |
| # 障害時は SHA タグを指定して pull すれば任意の時点に戻せる。 | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=sha | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| docker buildx imagetools create \ | |
| $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf 'ghcr.io/${{ github.repository }}-${{ matrix.image }}@sha256:%s ' *) | |
| - name: Inspect | |
| run: docker buildx imagetools inspect ghcr.io/${{ github.repository }}-${{ matrix.image }}:${{ steps.meta.outputs.version }} | |
| # 古い版を消して直近 10 世代だけ残す。 | |
| # | |
| # 公開パッケージなので GHCR の容量・転送は無料枠を消費しないが、版は放っておくと | |
| # 際限なく積み上がる(`latest` を付け替えても前の版は SHA タグ付きで残る)。 | |
| # 一覧が読めなくなるのと、どれが生きているか分からなくなるのを防ぐための掃除。 | |
| # | |
| # actions/delete-package-versions は使わない。 このリポジトリはマルチアーキで、 | |
| # 1 つのタグが manifest list + アーキごとの子イメージで構成される。あちらは | |
| # 子イメージを「タグ無しの版」として消してしまい、残したはずのタグが壊れる。 | |
| # この action は manifest list を理解して親子まとめて扱う。 | |
| - name: 古いイメージを削除して直近 10 世代だけ残す | |
| uses: dataaxiom/ghcr-cleanup-action@v1 | |
| with: | |
| # merge は image ごとの matrix なので、自分が作った版だけを掃除する | |
| # (3 つまとめて指定すると 3 並列で同じパッケージを触り合う)。 | |
| packages: chiezo-${{ matrix.image }} | |
| # ブリッジだけ世代を絞る。1 世代が圧縮後 300MB あり、CLI の版が上がるたびに | |
| # レイヤーが丸ごと入れ替わる(他の 2 つはベース層が世代間で共有されるので軽い)。 | |
| keep-n-tagged: ${{ matrix.image == 'bridge' && 3 || 10 }} | |
| delete-untagged: true | |
| # **焼きたてには触らせない。** 公開は 2 段階で、タグが付くのは merge の | |
| # とき —— その隙に掃除が走ると、公開待ちのイメージを消してしまう | |
| # (上の concurrency で実行は直列になるが、手動実行や外からの push など | |
| # 群の外から重なる道は塞げないので、掃除の側でも線を引く)。 | |
| # ここで見送ったぶんは、次の掃除で歳を取ってから消える。 | |
| older-than: 1 hour | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # Dependabot の PR のうち、破壊的でない更新(patch / minor)を自動でマージする。 | |
| # | |
| # needs が唯一のゲート —— ブランチ保護は掛けていない運用なので、 | |
| # 「CI が通ってからマージする」をジョブの依存関係で表している。 | |
| # major は自動で入れない(壊れ方が読めないので人が見る)。 | |
| auto-merge: | |
| needs: [lint, test] | |
| if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| # マージ後にイメージのビルドを起こすために要る(下の理由を参照) | |
| actions: write | |
| steps: | |
| - name: 更新の種類を読む | |
| id: meta | |
| uses: dependabot/fetch-metadata@v3 | |
| - name: patch / minor ならマージする | |
| if: steps.meta.outputs.update-type != 'version-update:semver-major' | |
| run: gh pr merge --squash --delete-branch "$PR_URL" | |
| env: | |
| PR_URL: ${{ github.event.pull_request.html_url }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # GITHUB_TOKEN で作った push は workflow を起こさないので、このままだと | |
| # 依存が main に入ってもイメージが作り直されない(build / merge はこの | |
| # ワークフローの中にある)。workflow_dispatch は GITHUB_TOKEN でも発火する | |
| # 例外なので、明示的に起こす | |
| - name: イメージのビルドを起こす | |
| if: steps.meta.outputs.update-type != 'version-update:semver-major' | |
| run: gh workflow run ci.yml --ref main | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |