Skip to content

ワーカーを、時計を待たずに起こせるようにする #329

ワーカーを、時計を待たずに起こせるようにする

ワーカーを、時計を待たずに起こせるようにする #329

Workflow file for this run

# 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 }}