xgent-image-push について
設定で指定した Harbor のプライベートレジストリへ Docker イメージを送る自己完結型スキルです。スクリプトが接続、タグ、アーキテクチャを確認し、認証情報の扱いも定めています。
主な機能
- 01設定または環境変数からレジストリ情報を取得
- 02接続、タグ競合、保持数を確認
- 03linux/amd64 イメージの構築と検証
- 04プッシュ後の検証とログアウト
- 05dry-run とローカルイメージの送信
活用シーン
- 01CIからHarborへアプリイメージを送信
- 02送信前にネットワークとタグを確認
- 03既存のローカルイメージをプライベートレジストリへ登録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ xgent-ai/ skills --skill xgent-image-pushインストール後、Claude Code で/xgent-image-pushを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.gitignoreregistry.env.examplescripts/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/s | runner 与仓库跨境(①) | 换同地域 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"要补,推你自己的版本即可。