mcpField
X
xgent-image-push

xgent-image-push

github.com/xgent-aiクラウド・DevOps1155 回インストール今日更新
リポジトリ

xgent-image-push について

設定で指定した Harbor のプライベートレジストリへ Docker イメージを送る自己完結型スキルです。スクリプトが接続、タグ、アーキテクチャを確認し、認証情報の扱いも定めています。

主な機能

  1. 01設定または環境変数からレジストリ情報を取得
  2. 02接続、タグ競合、保持数を確認
  3. 03linux/amd64 イメージの構築と検証
  4. 04プッシュ後の検証とログアウト
  5. 05dry-run とローカルイメージの送信

活用シーン

  1. 01CIからHarborへアプリイメージを送信
  2. 02送信前にネットワークとタグを確認
  3. 03既存のローカルイメージをプライベートレジストリへ登録

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/xgent-ai/skills --skill xgent-image-push

インストール後、Claude Code で/xgent-image-pushを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.gitignore
registry.env.example
scripts/
push-image.sh

詳細

イメージをプライベートレジストリにプッシュする

レジストリのアドレスはどこから取得するか(このスキルには含まれていません)

レジストリのドメインは内部情報のため、ここにはハードコードされていません。スクリプトは次の順に設定ファイルを探し、最初に見つかったものを使用します。

1) --config <path> 或 $XGENT_REGISTRY_CONFIG
2) ./.xgent-registry.env                        ← 项目内,要加进 .gitignore
3) ${XDG_CONFIG_HOME:-~/.config}/xgent/registry.env
4) ~/.xgent-registry.env
5) <skill 目录>/registry.env                    ← 同目录 registry.env.example 是模板

ファイルの形式は KEY=value です。必須項目は REGISTRY(プロトコルを含まないドメイン)と PROJECT(Harbor プロジェクト名)です。 同名の環境変数が優先されるため、CI では REGISTRY / PROJECT を直接設定すればよく、ファイルに保存する必要はありません。

設定ファイルが見つからない場合、スクリプトは停止し、作成方法をユーザーに案内します。この場合、ドメインを推測したり、別の場所から探したりしないでください。 運用担当者に問い合わせてもらうか、registry.env.example を案内してください。

完全な参照<REGISTRY>/<PROJECT>/<app>:<tag>
リポジトリ名App 名と同じです。運用担当者に確認し、独自に作らないでください
プッシュ用アカウントrobot$<PROJECT>+<app>。シークレットは CI secret にあります

プッシュする側は一か所にだけプッシュします。設定にある <REGISTRY> だけです。 書き込み先はここだけです。別のアドレスを独自に指定したり、"念のため"に二か所目へもう一度プッシュしたりしないでください。

⚠️ プルする側から見える host は、プッシュ先と異なる場合がありますが、パス部分はまったく同じです (<PROJECT>/<app>:<tag>)。配信は運用側の作業であり、プッシュする側には影響しません。 一度の push と一つの tag で十分です。相手がどこからプルするかを知る必要はありません。

⚠️ プッシュしてからデプロイ側でプルできるようになるまで、遅延が発生する場合があります(通常は数十秒、大きなイメージではさらに長くなります)。 その間、相手が取得するのは manifest unknown です。これは「まだ届いていない」という意味であり、「遅い」という意味ではありません。 ⇒ push の直後にデプロイ側を急かさないでください。さらに、すぐにプルできないからといって再プッシュしたり、別の tag でもう一度プッシュしたりしないでください。 使われない tag がレジストリに大量に残り、保持ポリシーの枠も消費してしまいます(④を参照)。

ワークフロー

1. まず三つのことを確認します(推測しないでください)
  • どの App か → リポジトリ名が決まります。間違えると denied エラーになります
  • どの tag か → 規則は v<MAJOR>.<MINOR>.<PATCH>-<7桁の git sha> です(例:v1.4.0-1a2b3c4。詳細は③を参照)。 sha の部分は機械的に取得でき、git rev-parse --short=7 HEAD で確認できます。 バージョン番号は必ずユーザーに確認してください。git tag、package.json、VERSION から推測したり、独断で patch の数字を +1 したりしないでください。 バージョン番号はユーザーのリリース履歴に記録されるもので、決めるのはユーザーです。推測が間違っていても、その場では誰も気づかないことがあります。 ユーザーがはっきり答えられない場合は、「今回は v いくつでリリースしますか」とそのまま聞いてください。tag は変更できないため、間違えてプッシュした場合は新しい tag を使うしかありません
  • 認証情報はどこから取得するか → ROBOT_USER / ROBOT_SECRET 環境変数、または CI secret を使います。ユーザーにシークレットを会話へ貼り付けるよう求めてはいけません。すでに貼り付けられている場合は、後でローテーションするよう伝えてください
2. スクリプトでプッシュします(推奨)

スクリプトはこの skill ディレクトリ内の scripts/push-image.sh にあります(絶対パスで呼び出してください。現在の作業ディレクトリを前提にしないでください)。

ROBOT_USER='robot$<project>+<app>' ROBOT_SECRET="$SECRET" \
  <skill 目录>/scripts/push-image.sh <app> <tag>

アドレスとプロジェクト名は上記の設定から取得するため、コマンドラインに記述する必要はありません。

スクリプトは次の順に処理します:接続の事前チェック → tag の競合チェック → 保持ポリシーの警告 → login → build(amd64 に固定)→ アーキテクチャの確認 → push → 検証 → logout。 いずれかの項目で問題があれば、その時点で停止します。失敗が確実な push に時間を費やすことはありません。

よく使うパラメーター:--config <path> · --context <dir> · --dockerfile <path> · --no-build(イメージはすでにローカルにあります)· --dry-run · --skip-link-check (まず --dry-run を実行してください。読み取り専用のチェックのみを行い、login/build/push は実行しません。)

この skill は自己完結型です。SKILL.md + scripts/push-image.sh + registry.env.example で構成され、 docker と curl 以外の外部スクリプトやリポジトリには依存しません。ディレクトリ全体を別の場所へコピーし、ローカル設定を一つ追加すれば使用できます。 ⚠️ 設定済みの registry.env / .xgent-registry.env はコミットしないでください(skill ディレクトリにある .gitignore がこの二つの名前を除外しています)。

3. スクリプトがない場合の同等な手動手順

まず設定から REGISTRY / PROJECT を取得します(ドメインを CI スクリプトにハードコードしないでください)。

IMAGE="${REGISTRY}/${PROJECT}/${APP}:${TAG}"
# 注意单引号 —— robot 名里的 $ 会被 shell 展开成空
echo "$ROBOT_SECRET" | docker login "$REGISTRY" -u 'robot$<project>+<app>' --password-stdin
docker build --platform linux/amd64 -t "$IMAGE" .
docker image inspect "$IMAGE" --format '{{.Os}}/{{.Architecture}}'   # 必须 linux/amd64
docker push "$IMAGE"
docker logout "$REGISTRY"

--password-stdin を使い、-p は使わないでください。後者はシェルの履歴や CI ログにシークレットを残してしまいます。

4. 結果を報告します

プッシュ後、ユーザーに完全なイメージ参照、digest、現在のリポジトリにある tag の数を伝えてください(10 個に近い場合は注意を促してください)。 ユーザーに代わってデプロイ側の「イメージレジストリアドレス」を変更したり、オーケストレーション層に触れたりしないでください。それはデプロイ/運用側の作業です。下記の境界を参照してください。

四つの厳守事項

この四つは実測で問題が発生したものです。最初の二つは失敗しても「プッシュ失敗」のようには見えないため、作業を始める前に防ぐ価値があります。

① CI はレジストリと同じ地域に置き、国境を越えてプッシュしないでください

国境を越える経路でのプッシュを実測したところ、速度は 49 KB/s、RTT は 440ms、パケット損失率は 35% でした。2.93GB のイメージに 17 時間かかります。 (2026-08-11 に本番機で実測。⚠️ これはその日、その経路での数値です。レジストリの移転や経路の変更後は再測定してください。そうしないと、根拠ではなく古い断言になってしまいます。) docker push が Retrying in N seconds のままタイムアウトまで止まり、ログを見ても原因がわからない、再試行しても解決しない、という症状になります。 回避方法はありません。runner の場所を変更するしかありません。

スクリプトは TLS ハンドシェイクの所要時間を測定して判断します(デフォルトでは >300ms を国境越えとみなします)。TCP ではなく TLS ハンドシェイクを使うのは、プロキシ経由の場合、TCP はローカルのプロキシに接続するだけで実際の距離を測定できない一方、TLS ハンドシェイクはレジストリ本体との間で行われ、トンネルを通しても距離が反映されるためです。 これは真のスループットではなく、あくまで代理指標ですが、440ms と同じ地域内の数十ミリ秒を見分けるには十分です。 ユーザーが試したいと言う場合は --skip-link-check を使えますが、先に上記の数値を説明してください。

プル方向の状態は良好です(同じ国境越えの経路で 7.2 MB/s)。そのため、「pull できるのだからネットワークに問題はない」という主張は成り立ちません。プッシュとプルの性能は対称ではありません。

② イメージは linux/amd64 でなければなりません

本番機は x86_64 です。Apple Silicon でそのまま docker build を実行すると arm64 が生成されます。 プッシュできて、プルもできるのに、コンテナは起動しません(exec format error)。レジストリの故障に見えますが、そうではありません。 そのため、ビルド時には --platform linux/amd64 を明示し、プッシュ前にもう一度 inspect して実際のアーキテクチャを確認してください。

③ tag は変更不可で、規則に従う必要があります

デプロイ側はイメージ参照の変更を検知して再構築を行います。同じ tag を上書きしても再構築は実行されません。本番環境では古いイメージが動き続けるのに、ユーザーは新しいものをリリースしたと思い込んでしまいます。また、問題が起きてもロールバック先がありません。そのため、スクリプトは既存の tag を検出すると、そのまま拒否します。 本当に上書きする必要がある場合は --force-tag を使い、再構築が自動的には行われないことをユーザーに理解してもらってください。

規則:v<MAJOR>.<MINOR>.<PATCH>-<7桁の git sha>。例:v1.4.0-1a2b3c4。 正式リリースでは sha の部分を省略できます(v1.4.0)。

一つの tag で三つの要件を同時に満たす必要があります。この形式はそれらを同時に満たす最もシンプルな形です。

要件満たすもの満たさない場合
一意 —— ビルドごとに新しい tag にするsha古い tag を上書きすると、再構築が実行されません(上記参照)
追跡可能 —— commit を逆引きできるsha本番で問題が起きたとき、どのコードのバージョンか調べられません
読みやすく、順序がわかる —— どちらが新しいか一目でわかるバージョン番号保持ポリシーで最も古いものを削除したり、前のバージョンを指定してロールバックしたりする際、sha だけでは順序がわかりません

バージョン番号の付け方はプッシュする側が決めることです。この skill が要求するのは tag の形式(一意、追跡可能、順序がわかる)だけで、minor と patch をいつ上げるかは定めません。ユーザーに代わって決めないでください。

tag 引数を指定せずにスクリプトを実行すると、現在の HEAD の sha が表示されます。それを使って組み立ててください。バージョン番号の部分は推測しません。

禁止されている tag(スクリプトが強制的に拒否):latest prod stable dev test main master rc など。 これらは変更可能なポインターを意味します。誰かが latest をプッシュした時点で、「参照は変わらないのに内容が変わる」状態になり、不変性の前提全体が崩れてしまいます。

例外:不经部署侧的项目可以开 latest(2026-08-27)

上面那条推理链的每一环都挂在「部署侧按镜像引用触发重建」上。给人手工 docker pull 的镜像(本地调试环境那一类,通常放在单独的项目里)没有部署侧、没有重建、没有回滚目标 —— 链断了,禁令的前提也就不在了。反过来还缺了点东西:新人第一次拉的时候, 他还没有渠道知道有哪些 tag,latest 补的正是这个洞。

配置里逐个列名打开(不是总开关,打错的 prod / main 仍然被拦):

MUTABLE_TAGS=latest

⚠️ 开了也不许 build 完直接推成 latest,脚本会拦。可变 tag 只能"移":

push-image.sh <app> v1.2.3-1a2b3c4                     # ① 先推不可变的那一份(真镜像)
push-image.sh <app> latest --point-at v1.2.3-1a2b3c4   # ② 再把指针移过去

第 ② 步是 OCI 分发规范里的重打 tag:把 v1.2.3-1a2b3c4 那一份的 manifest 原始字节取下来, 一个字节不改地 PUT 到 latest 上。digest 由字节算出来 ⇒ 必然与源相同 ⇒ 落在 同一个 artifact 上(不新增 artifact、不占保留策略名额、不动任何 blob,跨境也是一瞬)。 纯 registry API,不需要 docker,也不 login。移完会再拉一次服务端的 digest 核对。

⚠️ 别拿这两条顶替:

  • docker pull + docker tag + docker push —— 要把镜像字节拉下来再传上去(跨境 49KB/s), 而且 pull 只取本机架构那一份,多架构 manifest list 会被压扁成单架构。
  • docker buildx imagetools create —— 源是单架构 manifest 时它会包一层 index, digest 变了、Harbor 里多出一个 artifact。本脚本推的正是单架构镜像,会踩上。

为什么不许直接推:仓库的 GC 开着 delete_untagged。latest 一移走,上一个 digest 如果没有别的 tag 就成了 untagged,下一次 GC 连底层 blob 一起回收 ⇒ 这个 repository 永远只剩一份镜像,既没有历史也没有可回退的目标。这个后果不可逆,所以那条 die 没有留 --force 之类的绕过口子。

⚠️ 还要提醒拉取方:latest 变了他们不会知道。本地已有同名 latest 时 docker run / compose 默认不回仓库查 —— 让他们开工前先 docker pull, 要钉住某一版就用带版本号的 tag。

脚本还会核对 git 状态,两条都是踩过的坑:

  • 工作区不干净直接拒绝(--allow-dirty 放行)。带着未提交的改动构建,tag 指向的 commit 复现不出这个镜像 —— 出事时你会对着一个查不到的版本。
  • tag 里的 sha 与 HEAD 不符时告警。推错 commit 比推错 tag 更难发现:tag 看着挺对。

团队统一格式之后,把 TAG_PATTERN(正则)写进配置就从"提醒"升级为"强制"。 不配的时候只提醒不拦,是因为各团队可能还在用历史格式,不该由这个脚本单方面掐断发布。

④ 每个 repository 只保留最近推送的 10 个 tag

保留策略每天 02:00 跑,GC 每周日 03:00 回收存储。判据是"最近 push 的 10 个", 不是"最近在用的 10 个" —— 连着推 11 个测试 tag,会把生产正在跑的那个挤掉, 当时毫无征兆,等到 Pod 重调度、扩容或新节点加入那天才 ImagePullBackOff。

⇒ 调试镜像不要往生产 repo 推。tag 数接近 10 时提醒用户确认生产在跑的 tag 不在最老的那几个里。 需要更大的保留数要找运维改 RETAIN_LATEST_K,不要自己想办法绕。这个 registry 不是归档仓库。

报错对照

症状是什么怎么办
push 卡住 / 一直 Retrying in N seconds / 几十 KB/srunner 与仓库跨境(①)换同地域 runner。不是仓库故障,重试无效
unauthorized: unauthorized to access repository没 login / 密钥错 / robot 被吊销或轮换过重新 login;仍失败找运维查 robot 状态
denied: requested access to the resource is denied路径写错 —— 项目名必须是配置里的 PROJECT,repository 必须是 App 名核对 <REGISTRY>/<PROJECT>/<app>:<tag>
x509: certificate signed by unknown authority本机 CA 库或系统时间服务端是正式证书(含中间证书),正常机器不需要 --insecure-registry;配了是掩盖问题
拉得到但 exec format error架构不对(②)--platform linux/amd64 重建重推
之前推过的老 tag 不见了被保留策略挤掉(④)重推。长期留存另想办法
推到一半断了重推即可,已推上去的层会跳过⚠️ 别反复中断大镜像:每次中断在对象存储留未完成分片(运维侧配了 7 天清理)

密钥怎么处理

  • 只从环境变量/CI secret 取,不写进文件、不打印、不进 git
  • 用完 docker logout —— 别把凭证留在共享 runner 的 ~/.docker/config.json 里
  • ⚠️ Harbor 的 robot 权限粒度是项目级(已知偏差,做不到只到一个 repository): 密钥泄露 = 项目下所有 App 的 repository 都能被推/拉。所以不要放公共 runner、不要写进共享脚本。 能推别人的 repository 不等于可以推 —— 只推自己那一个
  • 轮换/吊销找运维;轮换会让旧密钥立刻失效,CI 换上新密钥前会一直红,要约时间

边界:这些不归推镜像的人管

  • 部署侧的「镜像仓库地址」设置、切换时机与停机窗口 —— 部署侧
  • 部署侧的拉取凭证 —— 运维侧。你手里那份是推的,两回事
  • registry 的可用性、证书、容量、GC —— 运维侧
  • 运维侧原有的镜像分发方式(如果之前有):切到 registry 之后不是降级路径 —— 仓库地址一非空,镜像名就成了完全限定名,旧的那条分支被永久跳过。别拿它当兜底

已经在生产跑着的 App(现在从别处拉镜像,只是要把仓库地址切到这里),第一次接入有一步容易漏: 除了推新版本,还要把当前在跑的那个 tag 原样推一份进来, 否则部署侧把镜像源切过来的那一刻拉不到同名 tag,切换即故障。

还没上过生产的新 App 没有这一步 —— 不存在"在跑的 tag"要补,推你自己的版本即可。

コメント