Claude Codeでpackage.xmlの差分デプロイ — git diffから生成して検証
git diffで変更ファイルを絞り、sf project generate manifestで差分のpackage.xmlを作って、validateまで進める手順です。Claude Codeに任せる範囲も示します。
Salesforceの差分デプロイ用package.xmlは、git diffで変更ファイルを絞り、その一覧をsf project generate manifest --source-dirに渡せば作れます。Claude Codeに任せるのは、この一覧と生成結果の突き合わせと、検証コマンドの実行までです。本番へのデプロイは人の手に残します。
この記事は、ソース形式(DX形式)のリポジトリでApexクラスや項目を変更したブランチを対象に、差分のpackage.xmlを作り、sf project deploy validateまたは--dry-runで確かめるまでを順に書きます。
差分package.xmlの考え方: source-dirに変更ファイルだけを渡す
sf project generate manifestは、メタデータの名前を列挙する(--metadata)か、ローカルのソースがあるパスを指定する(--source-dir)かのどちらかでmanifestを作ります。両方は同時に指定できません。
--source-dirにforce-appのようなディレクトリを渡せば、そのディレクトリの全コンポーネントがmanifestに入ります。差分にしたいなら、渡すパスを変更したファイルだけに絞ります。この絞り込みをgit diffが担います。
git diff(変更ファイルの一覧)
→ --source-dir に変更ファイルのパスを並べる
→ sf project generate manifest(差分のpackage.xml)
→ sf project deploy validate --manifest(検証)sf project deploy start系の--source-dirは、単一ファイルを渡すとそのファイルだけが対象になり、フォルダを渡すと配下のメタデータすべてが対象になります。変更のない兄弟ファイルまで巻き込まないよう、ファイル単位のパスで渡す意味はここにあります。
手順1: git diffで変更ファイルを種類別に分ける
まず、ベースブランチとの差分をステータス付きで見ます。次の出力は、手元で作った最小のDXリポジトリ(クラス1件の変更と削除、LWCの変更、項目の変更と追加、無関係なREADMEの変更)に対して、git 2.50.1で取ったものです。
git diff --name-status main...HEADM README.md
M force-app/main/default/classes/AccountService.cls
D force-app/main/default/classes/OldHelper.cls
D force-app/main/default/classes/OldHelper.cls-meta.xml
M force-app/main/default/lwc/hello/hello.html
M force-app/main/default/objects/Account/fields/Rank__c.field-meta.xml
A force-app/main/default/objects/Account/fields/Tier__c.field-meta.xmlここで分かれ目になるのが、先頭の文字です。A(追加)とM(変更)はデプロイの対象、D(削除)はpackage.xmlではなくdestructiveChangesの対象です。main...HEADの3点ドットは、2つのブランチの分岐点からの差分を見ます。ベースが先に進んでいても、自分のブランチで入れた変更だけが出ます。
デプロイ対象だけを取り出すには、--diff-filterで追加・コピー・変更・リネームに絞り、force-app配下に限定します。
git diff --name-only --diff-filter=ACMR main...HEAD -- force-appforce-app/main/default/classes/AccountService.cls
force-app/main/default/lwc/hello/hello.html
force-app/main/default/objects/Account/fields/Rank__c.field-meta.xml
force-app/main/default/objects/Account/fields/Tier__c.field-meta.xmlREADMEとOldHelperの削除が消え、デプロイ対象の4件が残りました。削除側は別に取ります。
git diff --name-only --diff-filter=D main...HEAD -- force-app手順2: 一覧を--source-dirに変換してmanifestを作る
一覧を--source-dirの繰り返しに変えるシェルスクリプトを置きます。LWCとAuraはフォルダ単位の「バンドル」なので、ファイル単位ではなくバンドルのディレクトリに丸めます。パスの区切りで5番目までを残す処理が、その丸めです。
#!/usr/bin/env bash
# make-manifest.sh — 変更ファイルから差分の package.xml を作る
set -euo pipefail
BASE="${1:-origin/main}"
args=()
while IFS= read -r p; do
case "$p" in
*/lwc/*|*/aura/*) p=$(echo "$p" | cut -d/ -f1-5) ;;
esac
args+=(--source-dir "$p")
done < <(git diff --name-only --diff-filter=ACMR "$BASE"...HEAD -- force-app | sort -u)
[ ${#args[@]} -gt 0 ] || { echo "デプロイ対象の変更なし"; exit 0; }
sf project generate manifest "${args[@]}" --output-dir manifest上のサンプルリポジトリでargsを組み立てた結果は、次の形になりました。LWCはファイルがバンドルのディレクトリforce-app/main/default/lwc/helloに丸められ、クラスと項目はファイルのままです。
--source-dir force-app/main/default/classes/AccountService.cls
--source-dir force-app/main/default/lwc/hello
--source-dir force-app/main/default/objects/Account/fields/Rank__c.field-meta.xml
--source-dir force-app/main/default/objects/Account/fields/Tier__c.field-meta.xmlsf project generate manifestは--output-dirで保存先を指定でき、--typeを省けばファイル名は既定のpackage.xmlになります。生成されるpackage.xmlは、次のような形が想定されます(型名と構成は例で、実際の出力はプロジェクトのAPIバージョンで変わります)。
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>AccountService</members>
<name>ApexClass</name>
</types>
<types>
<members>Account.Rank__c</members>
<members>Account.Tier__c</members>
<name>CustomField</name>
</types>
<types>
<members>hello</members>
<name>LightningComponentBundle</name>
</types>
<version>64.0</version>
</Package>上のXMLはsfの実出力ではなく、メタデータAPIの型名に沿った例示です。手元で生成したら、まず中身を目視してください。
手順3: validateかdry-runで本番前に確かめる
manifestができたら、デプロイを実行せずに確かめます。使うコマンドは、対象orgで変わります。
| 対象org | 使うコマンド | 理由 |
|---|---|---|
| 本番 | 使うコマンドsf project deploy validate --manifest manifest/package.xml | 理由Apexテストを必ず走らせ、ジョブIDを返す |
| Sandbox | 使うコマンドsf project deploy start --manifest manifest/package.xml --dry-run --test-level RunLocalTests | 理由Sandboxは既定でテストを走らせないため |
validateはデプロイに似ていますが、テスト実行が必須で、実際には反映せずにジョブIDを返します。検証に成功したジョブIDをsf project deploy quickに渡すと、テストを再実行せずに本番へ反映できます。ジョブIDの有効期間は、検証を始めてから10日間です。
validateはSandboxでの使用が想定されていません。Sandboxで同じ確認をしたいときは、表のとおり--dry-runと--test-level RunLocalTestsを組み合わせます。manifestの指定は、どちらも--manifestで、--source-dirや--metadataとは同時に使えません。
sf project deploy start --manifest manifest/package.xml \
--target-org dev-sandbox --dry-run --test-level RunLocalTestsテストの網羅については、カバレッジが75%を割るクラスを先に直しておくと検証で止まりにくくなります。手順はClaude CodeでApexテストクラスのカバレッジを75%に上げる手順にあります。
Claude Codeに任せる範囲を決める
ここまでの手順は、スクリプトだけでも回ります。Claude Codeを入れる価値があるのは、機械的に作ったpackage.xmlが変更の全体を表しているかを、diffと見比べて確かめる部分です。
claude -pは非対話で動くので、git diffの結果をパイプで渡せます。公式のheadlessの説明でも、diffをパイプで渡せばClaudeにBashの権限が要らないと書かれています。
git diff --name-status main...HEAD | claude --bare -p \
"このdiffと manifest/package.xml を突き合わせて。\
packageに無い変更と、destructiveChangesが要る削除を\
ファイル名つきで列挙。判断できないものは「要確認」と書く" \
--allowedTools "Read"--bareは、フックやCLAUDE.md、MCPサーバーの自動読み込みを省いて起動を速めるフラグです。スクリプトやSDKからの呼び出しで推奨されています。ファイル読み取りだけを許可しているので、このコマンドが書き換えやデプロイを行うことはありません。
manifestの生成とドライランまで任せるなら、許可を細かく書きます。--allowedToolsはBash(git diff *)のように前方一致で書けます。末尾の空白とアスタリスクが前方一致の印で、空白を抜くとgit diff-indexまで通ります。
claude --bare -p "make-manifest.sh を実行し、dev-sandbox にdry-runで検証して。\
失敗したら原因の候補を挙げるだけで、ファイルは直さない" \
--allowedTools "Read,Bash(./make-manifest.sh *),Bash(sf project deploy start *)"この許可は広すぎます。Bash(sf project deploy start *)は、--dry-runを付けないデプロイも通してしまいます。本番を巻き込まないための許可とフックの書き方は、Claude CodeでSalesforce CLIの権限を切るで扱っています。自動実行の入口を広げる前に、そちらの設定を先に入れてください。
CLAUDE.mdには、手順の方針だけを書いておきます。
## Salesforce 差分デプロイ
- package.xml は手で書かず、make-manifest.sh で作る
- 削除(git の D)は package.xml に入れない。destructiveChanges に分ける
- 検証は Sandbox なら --dry-run、本番は validate。deploy start は人が実行するCIで使うときのベースブランチの取り方
ローカルでは動いたスクリプトが、CIで空の差分を返すことがあります。原因の多くは、チェックアウトが浅い(shallow clone)ことです。origin/main...HEADは2つのブランチが共通の祖先でつながっていないと計算できません。
SGDのREADMEも、この罠を取り上げています。--fromと--toのそれぞれが有効なコミットに解決できても、取得した深さの中で履歴がつながらなければ、merge baseが見つからないというエラーになります。必要なのは、各refが解決できる深さではなく、2つのブランチが祖先を共有するまでの深さです。
GitHub Actionsのactions/checkoutなら、fetch-depth: 0で全履歴を取れます。
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: ./make-manifest.sh origin/mainもう一つ、pull_requestトリガーのチェックアウトは、PRのヘッドではなく合成されたマージコミットです。そのためmerge-base(HEAD, main)はmainの先頭を指し、3点ドットの差分とmerge baseを使う指定の意味が変わります。PRの変更だけを取りたいときは、イベントのペイロードにあるヘッドのコミットを--to相当として明示するか、そのコミットをチェックアウトします。
CIでclaude -pを回すときは、--bareで起動を速め、--allowedToolsは読み取りと--dry-runに限ります。結果を機械で扱うなら--output-format jsonを付け、jq -r '.result'で本文だけ取り出せます。
取りこぼしやすい変更
差分から作ったmanifestは、次の変更を落としやすくなります。
| 変更 | 起きること | 対処 |
|---|---|---|
ファイルの削除(D) | 起きること--source-dirに渡せず、package.xmlに載らない | 対処--type destroyでdestructiveChangesを別に作る |
| LWC・Auraのバンドル内の1ファイル | 起きることバンドルの一部のファイルだけを渡す形になる | 対処バンドルのディレクトリに丸める |
ファイル名の変更(R) | 起きること旧名の削除と新名の追加を両方扱う必要がある | 対処旧名側は削除として別に処理する |
| force-app外の変更 | 起きることforce-appの絞り込みから外れる | 対処複数のpackageDirectoriesを足して渡す |
削除用のmanifestは、メタデータ名を指定して作ります。公式のhelpの例では、--metadata ApexClass:MyApexClass --type destroyでdestructiveChanges.xmlが作られます。ファイルがすでに無いので、--source-dirは使えません。
sf project generate manifest --metadata ApexClass:OldHelper \
--type destroy --output-dir manifest--typeで作れるのは、package(package.xml)、pre(destructiveChangesPre.xml)、post(destructiveChangesPost.xml)、destroy(destructiveChanges.xml)の4種類です。実際のデプロイでは、--pre-destructive-changesと--post-destructive-changesで削除の実行順を選べます。
ファイル名の変更(R)を拾うには、--name-statusに-Mを付けて検出させる方法があります。ただ、リネームの検出は類似度の判定に左右されます。クラス名を変えたときは、旧名をdestructiveChangesに入れ忘れないよう、Claudeへの突き合わせの依頼にも「リネームを列挙」と書き添えるのが確実です。
自作スクリプトとsfdx-git-deltaの使い分け
差分manifestの生成には、コミュニティ製のSalesforce CLIプラグインsfdx-git-delta(SGD)もあります。sf sgd source delta --from <ref> --to HEADで、追加・変更分のpackage/package.xmlと、削除分のdestructiveChanges/destructiveChanges.xmlを一度に出力します。削除の扱いを自前で組まなくて済みます。
SGDのREADMEは、使える条件として、gitがソースの正であること、ソース形式であること、メタデータがアンマネージドであること(管理パッケージやアンロックドパッケージを作っていない)ことを挙げています。同時に、Salesforce公式のツールではなく自己責任で使うという注意書きもあります。
| 観点 | 自作スクリプト(本記事) | sfdx-git-delta |
|---|---|---|
| 提供元 | 自作スクリプト(本記事)Salesforce CLI標準のコマンドだけ | sfdx-git-deltaコミュニティ製プラグイン |
| 削除の扱い | 自作スクリプト(本記事)自分でdestroyのmanifestを作る | sfdx-git-deltadestructiveChangesを自動出力 |
| 追加の導入 | 自作スクリプト(本記事)不要 | sfdx-git-deltasfプラグインのインストール |
| 向く場面 | 自作スクリプト(本記事)小さなリポジトリ、依存を増やしたくない場合 | sfdx-git-delta継続的な増分デプロイをCIに組む場合 |
SGDを入れた後でも、Claude Codeの使いどころは変わりません。生成されたpackage.xmlを、diffと突き合わせるのです。diffを別の観点でレビューさせる流れは、Claude Codeでhelm diffのアップグレード前差分をレビューする手順と同じ型です。また、claude -pの権限やフラグの全体像はclaude -pの使い方にまとまっています。
まとめ
差分のpackage.xmlは、git diffで対象ファイルを絞り、--source-dirに渡して作ります。削除は別のdestructiveChangesに分け、検証は本番ならvalidate、Sandboxなら--dry-runを選びます。
Claude Codeには、manifestがdiffを覆っているかの突き合わせと、検証の実行までを任せます。許可の範囲は--allowedToolsで絞り、デプロイそのものは人が実行する運用が、いちばん事故が小さく済みます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。