🤖

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 つのチェックが走ります。

  1. バリデーションscripts/validate-skills.sh を実行し、frontmatter のフィールドが正しいか確認する
  2. バージョン更新チェック — ベースブランチと比較して、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 つあります。

  1. 採番の判断をなくす — スキルはプロンプトテキストの変更が主で、破壊的変更やメジャー/マイナーの区別が難しい。CalVer なら日付と連番だけで決まり、迷う必要がない。
  2. 自動化しやすい — 日付ベースなので、GitHub Actions から連番を計算してタグを打つ処理が単純に書ける。

2 種類のバージョン

このリポジトリには、バージョンが 2 箇所に付与されます。

対象 形式 付与のタイミング
SKILL.mdversion フィールド 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