gh skill コマンドで AI エージェントのスキルを一元管理・自動配布する
はじめに
Claude Code や GitHub Copilot CLI のような AI エージェントには「スキル」という仕組みがあります。スキルとは、特定のタスク(コミット作成、PR 作成など)の手順を記述したプロンプトファイルで、繰り返し使うワークフローを定義できます。
ただ、スキルを複数のリポジトリで使い回そうとすると、管理に困ることがあります。
- リポジトリごとにスキルファイルをコピーして置く必要がある
- スキルを更新しても、各リポジトリに反映するのが手間
- チームで使う場合、誰がどのスキルを持っているか把握しにくい
こうした課題を解決するため、GitHub CLI v2.90.0 から追加された gh skill コマンドと GitHub Actions を組み合わせて、スキルの一元管理・自動配布を実現しました。
この記事では、その仕組みと実装について紹介します。
リポジトリはこちらです: https://github.com/greendrop/agent-skills
gh skill コマンドとは
GitHub CLI v2.90.0 から gh skill コマンドが追加されました。このコマンドを使うと、GitHub リポジトリ上で管理しているスキルを他のリポジトリにインストールしたり、更新したりできます。
主なサブコマンドは以下の通りです。
| コマンド | 説明 |
|---|---|
gh skill publish |
リポジトリ上のスキルを公開する |
gh skill install |
スキルをインストールする |
gh skill update |
インストール済みのスキルを更新する |
プライベートリポジトリでも動作するため、個人用途だけでなく社内共有にも使えます。
リポジトリの構成
リポジトリの構成はシンプルです。スキルは <skill-name>/SKILL.md というパスに配置します。
agent-skills/
├── commit/
│ └── SKILL.md
├── create-pr/
│ └── SKILL.md
├── scripts/
│ └── validate-skills.sh
├── .github/
│ └── workflows/
│ ├── skill-publish.yml
│ ├── skill-validate.yml
│ └── skill-update.yml
└── mise.toml
SKILL.md の形式
各 SKILL.md には YAML frontmatter が必要です。
---
name: commit
description: 変更を確認し、意味のある単位に分けて Conventional Commits 形式でコミットを作成する
version: "2026.04.30.1"
source: "github.com/greendrop/agent-skills"
---
(スキルの本文。AI エージェントへのプロンプト)
name はディレクトリ名と一致させ、version は CalVer 形式(後述)、source はリポジトリのパスを固定値で指定します。
CI/CD の仕組み
このリポジトリでは GitHub Actions を使って、スキルのバリデーション・公開・更新を自動化しています。
skill-validate: PR 時のチェック
SKILL.md が変更された PR では、以下の 2 つのチェックが走ります。
-
バリデーション —
scripts/validate-skills.shを実行し、frontmatter のフィールドが正しいか確認する -
バージョン更新チェック — ベースブランチと比較して、
versionフィールドが更新されているか確認する
バージョン更新チェックのポイントは、git fetch を使わずに GitHub API 経由でベースブランチのファイル内容を取得している点です。
.github/workflows/skill-validate.yml
---
name: Skill Validate
on:
pull_request:
paths:
- "*/SKILL.md"
- "!.claude/**"
- "!.copilot/**"
- "!.agents/**"
- "scripts/validate-skills.sh"
- "mise.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-slim
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- name: Validate skills
run: mise run skills:validate
- name: Check version updated for modified SKILL.md files
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# GitHub API で PR の変更ファイル一覧を取得し、SKILL.md のみ抽出する。
# jq で "status filename" 形式の文字列にまとめて配列に格納する。
# git fetch が不要で、リモートの内容も API 経由で取得できる。
mapfile -t skill_entries < <(
gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
--paginate \
--jq '.[] | select(.filename | test("SKILL.md$")) | "\(.status) \(.filename)"'
)
if [[ ${#skill_entries[@]} -eq 0 ]]; then
echo "No SKILL.md files changed."
exit 0
fi
failed=0
for entry in "${skill_entries[@]}"; do
# "status filename" 形式から各フィールドを取り出す
status="${entry%% *}"
file="${entry#* }"
# 特定配下は管理外スキルのためスキップ
[[ "$file" == .claude/* ]] && continue
[[ "$file" == .copilot/* ]] && continue
[[ "$file" == .agents/* ]] && continue
# 新規追加ファイルは比較元が存在しないためスキップ
if [[ "$status" == "added" ]]; then
echo "New file: $file (skipping version check)"
continue
fi
# base ブランチのファイル内容を GitHub API で取得し base64 デコード後、
# awk で YAML frontmatter (--- ブロック) だけを抽出して version フィールドを読む
base_version=$(
gh api "repos/${{ github.repository }}/contents/$file?ref=${{ github.event.pull_request.base.sha }}" \
--jq '.content' | base64 -d \
| awk 'NR==1&&/^---$/{f=1;next}f&&/^---$/{exit}f{print}' \
| yq '.version // ""'
)
# 作業ツリーの現在のファイルから同様に version を取得する
current_version=$(
awk 'NR==1&&/^---$/{f=1;next}f&&/^---$/{exit}f{print}' "$file" \
| yq '.version // ""'
)
# version が更新されていない場合はエラー、更新されていれば成功とする
if [[ "$base_version" == "$current_version" ]]; then
echo "ERROR: version not updated in $file (still: $current_version)"
((failed++)) || true
else
echo "OK: $file version bumped ($base_version -> $current_version)"
fi
done
[[ $failed -eq 0 ]]
skill-publish: main マージ時の自動公開
main ブランチに SKILL.md の変更がマージされると、自動でスキルを公開します。
タグは CalVer 形式(v2026.04.30.1 など)で自動採番します。既存のタグを確認して連番を決定するため、同日に複数回公開しても重複しません。
.github/workflows/skill-publish.yml
---
name: Skill Publish
on:
push:
branches:
- main
paths:
- "*/SKILL.md"
- "!.claude/**"
- "!.copilot/**"
- "!.agents/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-slim
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- name: Publish skills
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# 当日分の CalVer タグを自動採番する(例: v2026.04.29.1)
today=$(TZ=Asia/Tokyo date +%Y.%m.%d)
last_n=$(
gh api "repos/${{ github.repository }}/tags" --paginate \
--jq "[.[] | select(.name | startswith(\"v${today}.\")) | .name | split(\".\")[-1] | tonumber] | max // 0"
)
next_n=$((last_n + 1))
tag="v${today}.${next_n}"
echo "Publishing: $tag"
gh skill publish --tag "$tag"
echo "Published: $tag"
日付は TZ=Asia/Tokyo を指定して JST 基準で採番しています。UTC 基準だと日本時間の午前 9 時より前にマージした場合、 前日の日付になってしまうためです。
skill-update: インストール済みスキルの自動更新
インストール先のリポジトリで使っているスキルが更新された場合、gh skill update --all で最新版に更新できます。
このワークフローは毎週月曜 09:00 JST に自動実行されるほか、手動でも実行できます。変更があれば PR を自動作成するため、スキルの更新を見落とすことがなくなります。
.github/workflows/skill-update.yml
---
name: Skill Update
on:
schedule:
- cron: "0 0 * * 0" # Monday 09:00 JST (Sunday 00:00 UTC)
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-slim
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- name: Update skills
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh skill update --all
- name: Check for changes
id: diff
run: |
set -euo pipefail
if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Generate GitHub App token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.GH_APPS_CREATE_PULL_REQEST_BOT_APP_ID }}
private-key: ${{ secrets.GH_APPS_CREATE_PULL_REQEST_BOT_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Commit and push branch
if: steps.diff.outputs.changed == 'true'
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
branch="skill-update/auto"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
git add -A
git commit -m "chore(skills): gh skill update --all を実行"
remote_url="https://x-access-token:${APP_TOKEN}@github.com/${{ github.repository }}.git"
git push "$remote_url" "${branch}:${branch}" --force-with-lease
- name: Create or update PR
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BODY: |
## 概要
`gh skill update --all` を実行してスキルを最新バージョンに自動更新しました。
## 変更内容
- 各スキルの `version` および `metadata` フィールドを最新版に更新
## 確認手順
- [ ] 各 SKILL.md のバージョン差分が意図したものか確認する
- [ ] 動作確認が必要なスキルについてローカルでテストする
run: |
set -euo pipefail
branch="skill-update/auto"
existing_pr=$(
gh pr list \
--head "$branch" \
--state open \
--json number \
--jq '.[0].number // ""'
)
if [[ -n "$existing_pr" ]]; then
echo "PR #${existing_pr} already exists for ${branch}; branch updated via force-with-lease."
else
gh pr create \
--title "chore(skills): gh skill update --all を実行" \
--body "$PR_BODY" \
--head "$branch" \
--base main
fi
バージョン管理の考え方
このリポジトリでは、セマンティックバージョニング(SemVer)ではなく カレンダーバージョニング(CalVer) を採用しています。
形式: YYYY.MM.DD.N(例: 2026.04.30.1)
理由は 2 つあります。
- 採番の判断をなくす — スキルはプロンプトテキストの変更が主で、破壊的変更やメジャー/マイナーの区別が難しい。CalVer なら日付と連番だけで決まり、迷う必要がない。
- 自動化しやすい — 日付ベースなので、GitHub Actions から連番を計算してタグを打つ処理が単純に書ける。
2 種類のバージョン
このリポジトリには、バージョンが 2 箇所に付与されます。
| 対象 | 形式 | 付与のタイミング |
|---|---|---|
SKILL.md の version フィールド |
YYYY.MM.DD.N |
SKILL.md を追加・更新するとき(人手) |
gh skill publish のタグ |
vYYYY.MM.DD.N |
main マージ時(GitHub Actions が自動付与) |
この 2 つのバージョンは一致している必要はありません。SKILL.md のバージョンはスキル自体のコンテンツのバージョンを表し、タグはリポジトリ全体の公開バージョンを表します。たとえば、複数のスキルをまとめて公開したとき、スキルごとの version はそれぞれ異なりますが、タグは 1 つで済みます。
スキルの使い方
インストール
gh skill install greendrop/agent-skills
インストール時にスキルを選択できるため、必要なものだけを取得できます。
インストールしたスキルは <ai-agent-tool-dir>/skills/<skill-name>/SKILL.md に配置されます。Claude Code の場合は .claude/skills/ です。
更新
gh skill update --all
インストール済みスキルの metadata フィールドに GitHub の参照情報が記録されており、これをもとに最新版を取得します。
GitHub Actions による自動更新
スキルを使っているリポジトリ側でも、GitHub Actions を使って定期的に更新 PR を自動作成できます。
このリポジトリでは skill-update ワークフローを用意しており、毎週月曜 09:00 JST に gh skill update --all を実行します。変更があれば自動でブランチを作成し、PR を起票します。
PR が作成されたらスキルの差分を確認してマージするだけなので、「気づいたら古いバージョンを使っていた」という状況を防げます。既に PR が開いている場合は重複作成せず、ブランチのみ更新するようにしています。
まとめ
gh skill コマンドと GitHub Actions を組み合わせることで、以下を実現しました。
- スキルをひとつのリポジトリで一元管理できる
- main マージで自動公開されるため、公開し忘れがない
- バージョン採番を自動化でき、手作業の判断が不要
- インストール先での更新も自動 PR で追いかけられる
スキルを定義して育てていくことで、AI エージェントとの作業がより快適になると感じています。プライベートリポジトリでも使えるため、チーム内での共有にも応用できそうです。
最後まで読んでいただきありがとうございます。この記事が少しでも役に立ったと思ったら、Like♥ を押していただけると励みになります。
Discussion