oinume journal

Scratchpad of what I learned

Claude Code GitHub Actionsでモデルを指定する

Claude Code GitHub Actionsを使っていて、自分は以下のような2種類のタスクを依頼することが多い。

  • 実装計画を立ててもらう
  • 実装をしてもらう

Claude Codeであれば実装計画はOpusを使って、実装タスクはSonnetを使うみたいな使い分けが簡単にできるのにClaude Code GitHub Actionsではデフォルトだとそれができなかったのでちょっとやってみた。

具体的には、以下のようなclaude.ymlのWorkflowファイルを追加して、GitHub Issueなどのコメントで @claude このタスクの実装計画を Opus で立てて とコメント内にopus, sonnet, haiku のキーワードを含めることで指定したモデルでClaudeが動くようになる。

やっていることは単純で、メッセージ内にopusなどのキーワードをgrepで引っ掛けてmodelを決めているだけ。注意点としてはgrep '\bopus\b'で検索しているため、「このタスクをopusでお願い」ではマッチしないようになっている。かわりに「このタスクを opus でお願い」のようにopusの前後にスペースを入れる必要がある。

そんなわけでClaude Code GitHub Actionsからもモデルが指定できるようになりタスクが捗るようになった(気がする)

claude.yml

name: claude-code
on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]
jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
      actions: read # Required for Claude to read CI results on PRs
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 1

      # Extract model from comment/issue body
      - name: Determine Claude Model
        id: determine-claude-model
        run: |
          # Get the comment/issue body based on event type
          if [ "${{ github.event_name }}" = "issue_comment" ]; then
            BODY=$(cat <<'EOF'
          ${{ github.event.comment.body }}
          EOF
          )
          elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
            BODY=$(cat <<'EOF'
          ${{ github.event.comment.body }}
          EOF
          )
          elif [ "${{ github.event_name }}" = "pull_request_review" ]; then
            BODY=$(cat <<'EOF'
          ${{ github.event.review.body }}
          EOF
          )
          elif [ "${{ github.event_name }}" = "issues" ]; then
            BODY=$(cat <<'EOF'
          ${{ github.event.issue.body }}
          EOF
          )
          fi
          
          # Convert to lowercase for case-insensitive matching
          BODY_LOWER=$(echo "$BODY" | tr '[:upper:]' '[:lower:]')

          # Determine model based on keywords
          if echo "$BODY_LOWER" | grep -q '\bopus\b'; then
            MODEL="claude-opus-4-1"
          elif echo "$BODY_LOWER" | grep -q '\bhaiku\b'; then
            MODEL="claude-haiku-4-5"
          else
            # Default to sonnet (including when 'sonnet' keyword is present)
            MODEL="claude-sonnet-4-5"
          fi

          echo "model=$MODEL" >> $GITHUB_OUTPUT
          echo "Selected model: $MODEL"

      - name: Run Claude Code
        id: run-claude-code
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

          # This is an optional setting that allows Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          allowed_bots: "claude-bot,claude"

          claude_args: |
            --model ${{ steps.determine-claude-model.outputs.model }}
            --allowedTools "Agent"
           (必要に応じてMCPなどを設定)

放送大学入学後に知った制度

放送大学に入学して約4ヶ月が経ったので、入学後に知った制度を書いておく(学割についてはたくさん情報があるけど、制度についての情報が少ないので)

ちなみに自分はきちんと単位を取って卒業する気はあまりなく、以下の2つの理由で入学した意識の低い学生です。

  • 気象学など気になる授業の動画を見たかった
  • 学割で色々安くなる

0. 全ての授業の動画が見れる

元々知ってはいたのだけど、大学院含めて全ての授業の動画が見れるのがすごい。YouTubeに上がっている動画を見る方がわかりやすいものもあるけど、これだけ多様な分野の動画が全て見れるというのはすごいと思う。

1. 大学のメールがGmail

(制度ではないけど完全に意外だったので書いておく)放送大学に入学すると <学生番号>@campus.ouj.ac.jp というメールアドレスが発行されるのだけど、これが実はGmail(Google Workspace)になっている。なので普段Gmail使っている自分にとってはとてもありがたかった。

2. 単位を落としても次の半期に無料で再試験が受けられる

放送大学では試験期間が決まっておりその間にWebから試験を受けるのだが、自分は運悪く風邪を引いてしまって試験が受けられず不合格になってしまった。そんな怠惰な人間のために再試験という制度があり、次の半期に無料で再試験が受けられるという神な制度。ちなみに放送大学は科目を履修すると1万2000円費用がかかるが、再試験の場合は履修が不要なので費用がかからない

再試験について

本学の学生の多くが有職者であることから、仕事の都合により受験の機会を逸したり、やむを得ず受験準備に必要な時間が取れなかった学生の利便を図るため、新規に科目登録した学期において、単位を修得出来なかった場合は、次の学期に学籍がある場合に限り(休学中の場合を除く)、次学期に再試験を受験できます(再試験に係る授業料等はかかりません)。

単位の認定基準(学部) | 放送大学 – BS放送・ネットで学ぶ通信制大学

3. 再入学時に以前に履修した単位を引き継げる

放送大学は最大で10年間在籍でき、その間に卒業できなかったとしても再入学すれば以前履修した単位をそのまま引き継げるらしい。また、再入学するときの入学金が25%割引になる。つまり入学金+履修選択した授業料を支払っていれば無限に在籍できるということ...! 自分のような1年に4単位ぐらいしか履修しないような堕落した生徒にとってこれはありがたい制度である。

放送大学は、卒業後や選科履修生、科目履修生で在学期間満了後でも再度入学ができます。修得済単位は全科履修生に入学時に卒業要件として認定されますので、卒業後に他コースに再入学をし、再度卒業を目指すこともできます。(全コースを卒業すること等により「名誉学生」の称号が付与されることもあります。)また選科履修生、科目履修生として再びご入学することもできます。

過去3年以内に教養学部に在籍していた方は入学料の25%割引が適用されます。 全科履修生・・・24,000円⇒18,000円 選科履修生・・・9,000円⇒6,750円 科目履修生・・・7,000円⇒5,250円

継続入学・再入学について | 放送大学

最後に

放送大学はいいぞ!

ghコマンドでよく使ってるヤツ

このポストがなぜかやたらバズっていたので、調子に乗って自分がよく使っているghコマンドのサブコマンドを書いてみる。

gh pr view -w

Usage

gh pr view [<number> | <url> | <branch>] [flags]

該当のpull requestをブラウザで開くために使っている。自分は何かとブラウザ上で作業することが多いのでかなり多用してる。

  • -w を省略するとターミナル上でPRの内容が表示される。
  • 引数の<number>などを省略するとカレントブランチのpull requestになる

gh pr checks --watch

Usage

 gh pr checks [<number> | <url> | <branch>] [flags]

カレントブランチのpull requestのchecks(CI)の状況を確認するのに使ってる。

  • gh pr checks --watch -i 3 のように-iでリフレッシュする間隔を秒数で指定できる。デフォルトは10秒
  • gh pr checks --json=name,state のように指定すると以下のようにJSON形式で出力できる
[
  {
    "name": "triage",
    "state": "SUCCESS"
  },
  {
    "name": "lint",
    "state": "SUCCESS"
  },
  {
    "name": "test",
    "state": "SUCCESS"
  }
]

gh pr checkout

Usage

gh pr checkout [<number> | <url> | <branch>] [flags]

主に他の人のpull requestのコードを自分のPC上でcheckoutしたい時に使う。

gh pr create

Usage

gh pr create [flags]

カレントブランチにコミットがある場合、gh pr create -w を実行するとコミットをpushしてpull request をopenする画面をブラウザで開いてくれる。(コメントで教えてもらってから重宝してます)

fzf との組み合わせ

fzfと組み合わせて以下のようなfunctionを定義している。

# fghpr - list pull requests with search query, then open selected pull-request with browser
# Use like `fghpr -A '@me' -s closed`
fghpr() {
    gh pr list "$@" --json number,title -q '.[] | "\(.number) \(.title)"' \
    | fzf --delimiter=' ' --with-nth=2.. \
    | awk '{print $1}' \
    | xargs -I {} gh pr view -w {}
}

例えば fghpr -A '@me' -s closed で自分が過去にマージ or クローズしたPRをターミナル上に一覧表示して、Fuzzy検索しつつ選択したものをブラウザで開くことができる。

Claude Code

あとはClaude Codeと共に使うと色々便利らしいです。

複数のプロダクトのリポジトリをMonorepoに移行する

表題の通り、技術スタックがほぼ同じプロダクト群のリポジトリを1つのMonorepoにまとめてみたという雑な記録。

元々は以下のようなPolyrepo構造になっていた。

  • product-a (repository)
    • backend
    • web
  • product-b (repository)
    • backend
    • native
    • web

これを以下のような構成に移行した。

  • products (repository)
    • apps
      • product-a
        • backend
        • web
      • product-b
        • backend
        • native
        • web

大まかには以下のような流れで移行する。

  1. 移行元のリポジトリで移行用ブランチを作成する
  2. 移行先のリポジトリで移行用ブランチを作成し、1.の移行用ブランチをmergeする

では実際に移行してみよう。product-aのリポジトリで以下のコマンドを実行する。

mkdir -p apps/product-a
git checkout -b monorepo
git mv -k * apps/product-a/  # -k is used to ignore the error that it can't move the projects directory
git mv -k .* apps/product-a/  # .* to move .gitignore and other dot files

次に移行先のproductsリポジトリで以下のコマンドを実行する。

git remote add -f product-a https://github.com/<owner>/product-a.git
git checkout -b import-product-a
git merge product-a/monorepo --allow-unrelated-histories

これで products リポジトリの import-product-aというブランチにproduct-aリポジトリのファイルが追加されているので、pull-requestを作るなどしてこのブランチをmainブランチにマージすればOK

# on main branch
git merge import-product-a

これを移行したいプロダクトの数だけ繰り返せばOKである。なお、.github/workflows などリポジトリ直下にないといけないディレクトリはMonorepoに取り込んだ後に手動でファイルを移動する必要があるので気をつけること。

というわけで私はこれで素敵なMonorepo lifeを送れるようになりました。おしまい。

RaycastのScript Commandsを試してみた

RaycastにはScript Commandsという機能があり、ずっとずっと気になっていたのでGW中に重い腰を上げて試してみた。最近会社の人にRaycastをオススメすることが多くなってきたのだけれども、「Raycast歴1年以上経つのにScript Commandsも使ったことないの?」みたいに後ろ指差されないようにするためにも...

Script Commandsとは?

簡単にいうとCUIのコマンドをRaycastから呼び出せるというただそれだけのものっぽい。「っぽい」というのは単純に自分がちゃんと調べてないだけです。はいテキトーですいません。

なにを作るか?

Hello Worldを出力するだけのScript Commandsを作っても仕方がないので、よく自分でやっている単純作業を自動化するスクリプトを考えてみた。結果として生まれたのは、以下のような beforeの文字列をafterに整形するだけのスクリプトである。なんでこんなものが必要かというと、スプレッドシートからIDなどのリストをコピーしてSQLのWHERE句のINに指定する、ということをよくやっているから。

before

aaa
bbb
ccc

after

'aaa',
'bbb',
'ccc',

Script Commandsの登録方法

めちゃくちゃ簡単なのでこのページを見てもらうのが良いと思う。テキストで手順を書くと以下の4ステップしかない。

  1. Raycastを起動する
  2. Create Script Commandと入力する
  3. 必要な情報を入力する
  4. スクリプトにコマンドを記述する

ちなみに上で書いたScript Commandのスクリプトは以下のような感じである。

#!/bin/bash

# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title single-quote-with-trailing-comma
# @raycast.mode fullOutput

# Optional parameters:
# @raycast.icon 🤖
# @raycast.needsConfirmation true

# Documentation:
# @raycast.description single-quote-with-trailing-comma
# @raycast.author oinume
# @raycast.authorURL https://github.com/oinume

pbpaste | perl -pe "s/.*/'\$&',/"

作成したScript Commandを呼び出す

あとはRaycastから作成したsingle-quote-with-trailing-commaを呼び出すだけである。以下の動画ではsingle-quote-with-trailing-commaによって生成された出力をクリップボードにコピーしている。

最後に一言

というわけで駆け足でScript Commandsについて説明した。Apple Scriptを使って特定のSlack Channelを開くということもできるらしいので夢は広がりそうである。Raycast、本当によくできていてこれがあるおかげでmacOSから離れられない。次はExtensionの作成にチャレンジしてみたい。