feat: Alpine ベースの Claude Code リモート実行 Helm チャート初版 (v2.1.259)
gateway(nginx) + manager(python 標準ライブラリ) + セッションごとの使い捨て Pod + reaper CronJob の構成。ブラウザ(PC/スマホ)からアクセスし、セッション単位で 独立した Alpine コンテナに Git リポジトリ(GitHub/Gitea など)をチェックアウトして 編集・検証・プッシュする。無操作が続いたセッションは Pod ごと自動破棄。 - セキュリティ: セッション NS 分離 / restricted PodSecurity / RBAC はセッション NS の pods,services のみの Role / セッション Pod は SA トークン非マウント・非 root・ readOnlyRootFilesystem・drop ALL caps・seccomp / NetworkPolicy で egress を DNS+HTTPS+SSH に限定 / Git ホスト allow-list / ResourceQuota+LimitRange - ライフサイクル: idleTimeout(Pod 内ウォッチドッグ + manager 回収) / maxLifetime (activeDeadlineSeconds) / maxSessions / 予備の孤児掃除 CronJob - 認証: token(既定) / basic / forward(外部SSO) / none。WebSocket は Cookie トークン - Claude 認証: ANTHROPIC_API_KEY または CLAUDE_CODE_OAUTH_TOKEN - CI: helm-release.yaml(未リリース版のみ tag+release+publish) / image-update-and-release.yaml(週次 npm 追従) / session-image.yaml(multi-arch build) - files/ 実体を .Files.Get で ConfigMap 化。helm lint / template 検証済み
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# .gitea/workflows/helm-release.yaml
|
||||
# プロジェクトへの変更(main への push)時に実行。
|
||||
# Chart.yaml の version が「まだリリースされていない」場合のみ、
|
||||
# タグ作成 → Gitea リリース → Helm チャート publish を行う。
|
||||
# 既にリリース済みのバージョンなら lint のみで終了する(再 publish しない)。
|
||||
|
||||
name: Helm Chart Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY_URL: https://git.cafepieters.com
|
||||
OWNER: helmchart
|
||||
|
||||
jobs:
|
||||
release-chart:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: 'v3.16.4'
|
||||
|
||||
- name: Ensure jq
|
||||
run: command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
- name: Get chart info
|
||||
id: info
|
||||
run: |
|
||||
set -e
|
||||
CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}')
|
||||
CHART_VERSION=$(grep '^version:' Chart.yaml | awk '{print $2}' | tr -d '"')
|
||||
echo "name=${CHART_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "version=${CHART_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Chart: ${CHART_NAME} ${CHART_VERSION}"
|
||||
|
||||
- name: Lint
|
||||
run: helm lint .
|
||||
|
||||
- name: Check whether this version is already released
|
||||
id: check
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
V="v${{ steps.info.outputs.version }}"
|
||||
# 1) Gitea リリースの有無
|
||||
REL_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${V}" | jq -r '.id // empty')
|
||||
# 2) Package Registry 上の有無
|
||||
PKG_HTTP=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
-u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"${REGISTRY_URL}/api/packages/${OWNER}/helm/${{ steps.info.outputs.name }}/${{ steps.info.outputs.version }}")
|
||||
|
||||
if [ -n "$REL_ID" ] || [ "$PKG_HTTP" = "200" ]; then
|
||||
echo "release_needed=false" >> $GITHUB_OUTPUT
|
||||
echo "既に ${V} はリリース済み(release_id=${REL_ID:-none}, pkg_http=${PKG_HTTP})。publish をスキップします。"
|
||||
else
|
||||
echo "release_needed=true" >> $GITHUB_OUTPUT
|
||||
echo "${V} は未リリース。リリースを実行します。"
|
||||
fi
|
||||
|
||||
- name: Package
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
helm package .
|
||||
echo "CHART_FILE=$(ls ${{ steps.info.outputs.name }}-*.tgz)" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Git tag
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
git config user.name "Claude"
|
||||
git config user.email "claude@cafepieters.com"
|
||||
V="v${{ steps.info.outputs.version }}"
|
||||
if git rev-parse "$V" >/dev/null 2>&1; then
|
||||
echo "tag $V exists, skip"
|
||||
else
|
||||
git tag -a "$V" -m "Release $V"
|
||||
git push origin "$V"
|
||||
fi
|
||||
|
||||
- name: Create Gitea release
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
V="v${{ steps.info.outputs.version }}"
|
||||
BODY="${{ steps.info.outputs.name }} Helm Chart ${V}"
|
||||
RID=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${V}\",\"name\":\"${V}\",\"body\":\"${BODY}\"}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" | jq -r '.id // empty')
|
||||
if [ -z "$RID" ]; then
|
||||
RID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${V}" | jq -r '.id // empty')
|
||||
fi
|
||||
curl --fail-with-body -X POST -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/gzip" --data-binary "@${CHART_FILE}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RID}/assets?name=${CHART_FILE}"
|
||||
echo "Gitea release ${V} 作成完了"
|
||||
|
||||
- name: Publish to Gitea Package Registry
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
curl --fail-with-body -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
-X POST --upload-file "${CHART_FILE}" \
|
||||
"${REGISTRY_URL}/api/packages/${OWNER}/helm/api/charts"
|
||||
echo "✅ ${CHART_FILE} を publish しました"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "================================"
|
||||
echo "Chart : ${{ steps.info.outputs.name }} ${{ steps.info.outputs.version }}"
|
||||
echo "Released : ${{ steps.check.outputs.release_needed }}"
|
||||
echo "================================"
|
||||
echo "helm repo add cafepieters ${REGISTRY_URL}/api/packages/${OWNER}/helm"
|
||||
echo "helm repo update && helm install my-${{ steps.info.outputs.name }} cafepieters/${{ steps.info.outputs.name }}"
|
||||
@@ -0,0 +1,188 @@
|
||||
name: Update Versions and Release Helm Chart
|
||||
|
||||
# 毎週月曜 3:00 AM JST(日曜 18:00 UTC)に実行。
|
||||
# - 主アプリ = Claude Code CLI(npm: @anthropic-ai/claude-code)の最新版を確認
|
||||
# - base image = gateway(nginx) / manager(python) の最新 Alpine タグを確認
|
||||
# - 主アプリが更新: values.yaml / Chart.yaml / README 履歴を更新しコミット → タグ + Gitea リリース + チャート publish
|
||||
# - base image のみ更新: values.yaml を更新してコミットのみ(リリースなし)
|
||||
#
|
||||
# セッションイメージのビルドは session-image.yaml が担当する(このワークフローが
|
||||
# Chart.yaml を push した結果として発火する)。
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 18 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY_URL: https://git.cafepieters.com
|
||||
OWNER: helmchart
|
||||
|
||||
jobs:
|
||||
update-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: 'v3.16.4'
|
||||
|
||||
- name: Ensure jq
|
||||
run: command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
- name: Check Claude Code (npm) version
|
||||
id: cc
|
||||
run: |
|
||||
set -e
|
||||
CURRENT=$(grep '^appVersion:' Chart.yaml | sed 's/.*"\(.*\)".*/\1/')
|
||||
LATEST=$(curl -s --retry 3 --retry-delay 5 \
|
||||
"https://registry.npmjs.org/@anthropic-ai/claude-code/latest" | jq -r '.version // empty')
|
||||
echo "Current: $CURRENT / npm latest: ${LATEST:-not found}"
|
||||
|
||||
if [ -n "$LATEST" ]; then
|
||||
HIGHER=$(printf '%s\n%s' "$CURRENT" "$LATEST" | sort -V | tail -1)
|
||||
if [ "$HIGHER" != "$LATEST" ] && [ "$CURRENT" != "$LATEST" ]; then
|
||||
echo "Warning: npm latest ($LATEST) < current ($CURRENT). Skipping."
|
||||
LATEST="$CURRENT"
|
||||
fi
|
||||
else
|
||||
LATEST="$CURRENT"
|
||||
fi
|
||||
echo "current=$CURRENT" >> $GITHUB_OUTPUT
|
||||
echo "latest=$LATEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check base image (nginx / python) tags
|
||||
id: base
|
||||
run: |
|
||||
set -e
|
||||
NGINX_CUR=$(grep -A3 'gateway:' values.yaml | grep 'tag:' | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
PY_CUR=$(grep -A3 'manager:' values.yaml | grep 'tag:' | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
|
||||
NGINX_LATEST=$(curl -s --retry 3 "https://registry.hub.docker.com/v2/repositories/nginxinc/nginx-unprivileged/tags?page_size=100" \
|
||||
| jq -r '.results[].name' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-alpine$' | sort -V | tail -1)
|
||||
PY_LATEST=$(curl -s --retry 3 "https://registry.hub.docker.com/v2/repositories/library/python/tags?page_size=100&name=alpine" \
|
||||
| jq -r '.results[].name' | grep -E '^3\.[0-9]+-alpine$' | sort -V | tail -1)
|
||||
|
||||
[ -n "$NGINX_LATEST" ] || NGINX_LATEST="$NGINX_CUR"
|
||||
[ -n "$PY_LATEST" ] || PY_LATEST="$PY_CUR"
|
||||
echo "nginx: $NGINX_CUR -> $NGINX_LATEST ; python: $PY_CUR -> $PY_LATEST"
|
||||
echo "nginx_cur=$NGINX_CUR" >> $GITHUB_OUTPUT
|
||||
echo "nginx_latest=$NGINX_LATEST" >> $GITHUB_OUTPUT
|
||||
echo "py_cur=$PY_CUR" >> $GITHUB_OUTPUT
|
||||
echo "py_latest=$PY_LATEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Decide
|
||||
id: decide
|
||||
run: |
|
||||
set -e
|
||||
CC=false; BASE=false
|
||||
[ "${{ steps.cc.outputs.current }}" != "${{ steps.cc.outputs.latest }}" ] && CC=true
|
||||
[ "${{ steps.base.outputs.nginx_cur }}" != "${{ steps.base.outputs.nginx_latest }}" ] && BASE=true
|
||||
[ "${{ steps.base.outputs.py_cur }}" != "${{ steps.base.outputs.py_latest }}" ] && BASE=true
|
||||
echo "cc=$CC" >> $GITHUB_OUTPUT
|
||||
echo "base=$BASE" >> $GITHUB_OUTPUT
|
||||
echo "update_needed=$([ "$CC" = true ] || [ "$BASE" = true ] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update values.yaml (image tags)
|
||||
if: steps.decide.outputs.update_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
sed -i "s|tag: \"${{ steps.base.outputs.nginx_cur }}\"|tag: \"${{ steps.base.outputs.nginx_latest }}\"|g" values.yaml
|
||||
sed -i "s|tag: \"${{ steps.base.outputs.py_cur }}\"|tag: \"${{ steps.base.outputs.py_latest }}\"|g" values.yaml
|
||||
if [ "${{ steps.decide.outputs.cc }}" = "true" ]; then
|
||||
sed -i "s|tag: \"${{ steps.cc.outputs.current }}\"|tag: \"${{ steps.cc.outputs.latest }}\"|g" values.yaml
|
||||
fi
|
||||
git diff --stat values.yaml || true
|
||||
|
||||
- name: Update Chart.yaml (version == Claude Code version)
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
NEW="${{ steps.cc.outputs.latest }}"
|
||||
sed -i "s/^version: .*/version: ${NEW}/" Chart.yaml
|
||||
sed -i "s/^appVersion: .*/appVersion: \"${NEW}\"/" Chart.yaml
|
||||
cat Chart.yaml
|
||||
|
||||
- name: Update README version history
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
NEW="${{ steps.cc.outputs.latest }}"
|
||||
sed -i "/^| バージョン | Claude Code | 変更内容 |/{
|
||||
n
|
||||
a\| ${NEW} | ${NEW} | 自動更新 |
|
||||
}" README.md
|
||||
|
||||
- name: Lint
|
||||
if: steps.decide.outputs.update_needed == 'true'
|
||||
run: helm lint .
|
||||
|
||||
- name: Commit
|
||||
if: steps.decide.outputs.update_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
git config user.name "Claude"
|
||||
git config user.email "claude@cafepieters.com"
|
||||
if [ "${{ steps.decide.outputs.cc }}" = "true" ]; then
|
||||
git add values.yaml Chart.yaml README.md
|
||||
git commit -m "chore: update Claude Code to ${{ steps.cc.outputs.latest }} (nginx ${{ steps.base.outputs.nginx_latest }}, python ${{ steps.base.outputs.py_latest }})"
|
||||
else
|
||||
git add values.yaml
|
||||
git commit -m "chore: bump base images — nginx ${{ steps.base.outputs.nginx_latest }}, python ${{ steps.base.outputs.py_latest }} (no release)"
|
||||
fi
|
||||
git push origin "${GITHUB_REF_NAME:-main}"
|
||||
|
||||
- name: Package
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
helm package .
|
||||
echo "CHART_FILE=$(ls claude-code-*.tgz)" >> $GITHUB_ENV
|
||||
|
||||
- name: Git tag
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
V="v${{ steps.cc.outputs.latest }}"
|
||||
git rev-parse "$V" >/dev/null 2>&1 && { echo "tag $V exists"; exit 0; }
|
||||
git tag -a "$V" -m "Release $V"
|
||||
git push origin "$V"
|
||||
|
||||
- name: Gitea Release
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
NEW="${{ steps.cc.outputs.latest }}"; V="v${NEW}"
|
||||
if curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${V}" | jq -e '.id' >/dev/null 2>&1; then
|
||||
echo "release ${V} exists, skip"
|
||||
else
|
||||
RID=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${V}\",\"name\":\"${V}\",\"body\":\"claude-code Helm Chart ${V} — Claude Code CLI ${NEW}\"}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" | jq -r '.id')
|
||||
curl --fail-with-body -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/gzip" \
|
||||
--data-binary "@${CHART_FILE}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RID}/assets?name=${CHART_FILE}"
|
||||
fi
|
||||
|
||||
- name: Publish chart to Gitea Package Registry
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
curl --fail-with-body -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
-X POST --upload-file "${CHART_FILE}" \
|
||||
"${REGISTRY_URL}/api/packages/${OWNER}/helm/api/charts"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "Claude Code : ${{ steps.cc.outputs.current }} -> ${{ steps.cc.outputs.latest }} (updated: ${{ steps.decide.outputs.cc }})"
|
||||
echo "nginx : ${{ steps.base.outputs.nginx_cur }} -> ${{ steps.base.outputs.nginx_latest }}"
|
||||
echo "python : ${{ steps.base.outputs.py_cur }} -> ${{ steps.base.outputs.py_latest }}"
|
||||
echo "release : ${{ steps.decide.outputs.cc }}"
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Build Session Image
|
||||
|
||||
# セッション用の事前ビルドイメージ(git + openssh + ttyd + node + @anthropic-ai/claude-code)を
|
||||
# linux/amd64 + linux/arm64 でビルドし、Gitea Container Registry へ push する。
|
||||
#
|
||||
# トリガー:
|
||||
# - files/Dockerfile.session / files/session-entrypoint.sh / Chart.yaml の変更 push
|
||||
# (image-update-and-release.yaml が Chart.yaml を更新した後にも発火する)
|
||||
# - 手動実行(tag を任意指定可能)
|
||||
#
|
||||
# タグ: Chart.yaml の appVersion(= Claude Code CLI バージョン) と latest。
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- 'files/Dockerfile.session'
|
||||
- 'files/session-entrypoint.sh'
|
||||
- 'Chart.yaml'
|
||||
- '.gitea/workflows/session-image.yaml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'イメージタグ(未指定なら Chart.yaml の appVersion)'
|
||||
required: false
|
||||
|
||||
env:
|
||||
REGISTRY_HOST: git.cafepieters.com
|
||||
IMAGE: git.cafepieters.com/helmchart/claude-code/session
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Resolve tag
|
||||
id: t
|
||||
run: |
|
||||
set -e
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
[ -n "$TAG" ] || TAG=$(grep '^appVersion:' Chart.yaml | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "Session image tag: $TAG"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_HOST }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build & push (multi-arch)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: files/Dockerfile.session
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
CLAUDE_CODE_VERSION=${{ steps.t.outputs.tag }}
|
||||
tags: |
|
||||
${{ env.IMAGE }}:${{ steps.t.outputs.tag }}
|
||||
${{ env.IMAGE }}:latest
|
||||
push: true
|
||||
provenance: false
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "pushed ${IMAGE}:${{ steps.t.outputs.tag }} (+ latest) for linux/amd64,linux/arm64"
|
||||
Reference in New Issue
Block a user