
はじめに
こんにちは。
UPSIDERでモバイルアプリの開発を担当している App team です。
App team では現在、UPSIDER App と PRESIDENT CARD App の 2 つのモバイルアプリを開発しており、2週間のスプリントで定期的にリリースを行っています。
本記事では、モバイルアプリのリリース時におけるシステム変更管理を自動化した取り組みについて、その運用背景と課題、そして仕組みの概要を紹介します。
リリース運用とシステム変更について
UPSIDER では、リリース時のシステム変更管理を全社的に行っています。これは、金融系サービスを提供する特性上、プロダクトのリリースには高い安全性と透明性が求められるためです。リリース時にはシステムに対する変更内容を文書化し、リリース可否を判断するための材料として利用しています。
システム変更の目的
システムの変更内容を共有・レビューする目的は、以下の2点にあります。
- リリース可否判断の材料
- システム変更内容を文書化し、リリースを実行するかどうかの判断材料として活用されています。
- 安全性と透明性の確保
- 「何が・なぜ・どのように変わったのか」を第三者が確認できる状態にすることで、リリースの安全性と透明性を確保しています。
管理方法と記載内容
システム変更は Notion 上のデータベースで管理しています。システム変更には、システム変更の背景や内容に加えて、紐づくPull Request や Linear チケットの情報を記載しています。
主に以下のような情報を記載しています。
- Why
- 現在の課題や、誰にどのようなメリットがあるか
- What
- システム上で具体的に何が変更されるのか
- Risk
- 変更によって生じる影響度
- 関連する Pull Request / Linear チケット
これらはリリース単位で統合された一覧として管理され、全てのリリースは承認フローを経て運用されています。
課題
システム変更の作成は、開発者にとって大きな負担になっていました。 リリース準備の際には、以下の作業が発生します。
- 対象期間にマージされた Pull Request の洗い出し
- Pull Request に紐づく Linear チケットの確認
- Linear チケットや Pull Request の内容をもとに、システム変更への転記
- Why / What / Risk の記載
多いときには 1 回のリリースで 20 件以上のシステム変更を作成する必要があります。
2 週間ごとにリリースを行う中で、開発者や Pull Request が増えるにつれて、この作業はさらに煩雑になっていました。
結果として、本来開発に使いたい時間がリリース準備に取られてしまう という課題がありました。
解決策
そこで、システム変更の作成作業を可能な限り AI に任せ、開発工数を本来の開発に振り向けることを目指しました。今回の自動化のポイントは次の 2 点です。
- Git リポジトリにアクセスし、前回リリースとの差分を検出すること
- Notion MCP を利用して Notion へのページ作成を自動化すること
これらを組み合わせ、Cursor 上で完結する仕組み として実装しました。 自動化によって Why / What など Pull Request の説明文などから取得できる部分は AI によって生成することで、エンジニアは Risk 評価と内容のレビューに注力し、安全なリリースにより時間をかけられるようになります。
この仕組みでは、以下の技術スタックを利用しています。
Git / GitHub CLI
前回リリースとの差分検出および Pull Request 情報の取得に使用します。Linear MCP
Pull Request に紐づくチケット情報の取得に使用します。Notion MCP
System Changes データベースへのページ作成・編集を自動化します。
仕組みは大きく 6 つのステップに分かれており、ここからは各 Step 毎にプロンプトなども交えながら説明していきます。
Step 1: 事前準備
まず初めに、GitHub・Notion・Linear の各ツールに対する認証と接続状態を確認し、不備があれば処理を中断して、MCPの設定を促します。
以下は生成に使用しているプロンプトの例です。
1. Check GitHub CLI: Execute `gh auth status` to verify that GitHub CLI is configured and authenticated. 2. Check Notion MCP: Execute `notion-get-self` to verify that bot user information can be retrieved. 3. Check Linear MCP: Execute `list_teams` to verify that team list can be retrieved. 4. If any check fails, ask the user to check the configuration and stop processing.
Step 2: Pull Request 一覧の生成
このステップでは、前回リリース(BASE_BRANCH)と 今回リリース(TARGET_BRANCH)をもとに、GitHub CLI を使用して、比較対象の差分に含まれるマージ済み Pull Request を抽出し、Pull Request 一覧を以下のようなプロンプトを用いて出力します。出力結果として、タイトル、作成者、Linear チケットへのリンクなどを受け取ります。
1. Ask the user for the comparison targets:
- BASE_BRANCH (base branch or tag. Examples: `main`, `releases/v1.0.0`, `v1.0.0`)
- TARGET_BRANCH (target branch or tag. Examples: `releases/v1.1.0`, `v1.1.0`)
2. Execute the following command:
`./scripts/create_release_pr_list.sh <BASE_BRANCH> <TARGET_BRANCH>
- Note: This script automatically determines whether the reference is a branch or tag
3. Verify the generated file:
`/results/step2_result_release_diff_prs.md`
プロンプト中の create_release_pr_list.sh は、Pull Request の抽出作業において出力の均一化と効率化のために、あらかじめ定義したものを使用しています。この script は以下のようなものです。
#!/usr/bin/env bash
set -euo pipefail
# 1. 引数と設定
BASE_REF="${1:?Base ref required}"
TARGET_REF="${2:?Target ref required}"
REPO="upsider/app"
# 2. 差分から PR 番号を抽出
git fetch origin --tags -q
PRS=$(git log "origin/${BASE_REF}..origin/${TARGET_REF}" --merges --pretty='%s' |
sed -nE 's/.*#([0-9]+).*/\1/p' | sort -n | uniq)
echo "| PR | Title | Author | Linear URL |"
echo "| --- | --- | --- | --- |"
# 3. 各PRの詳細を GitHub CLI で取得して Markdown 出力
for PR in ${PRS}; do
gh pr view "${PR}" --repo "${REPO}" --json number,title,author,body,url,comments --template '
{{- $body := .body -}}
{{- $title := .title -}}
{{- $url := .url -}}
{{- $author := .author.login -}}
{{- /* Body と Title から Linear のチケット ID を抽出 */ -}}
{{- $ticket := or (match $body "A-[0-9]+") (match $title "A-[0-9]+") -}}
{{- $ticket_id := (index $ticket 0 | default "-") -}}
{{- $ticket_url := (if ne $ticket_id "-" then (printf "https://linear.app/*****/issue/%s/" $ticket_id) else "-") -}}
| [#{{.number}}]({{$url}}) | {{$title}} | {{$author}} | {{$ticket_url}} |
'
done
MCP の動作が不安定だったり途中で session が切れたりした場合に復帰を容易にするため、変更を step 毎に markdown として出力しています。この step では step2_result_release_diff_prs.md という形で出力しており、結果は以下のような形になります。
| PR | Title | Author | Linear URL | | --- | --- | --- | --- | | #237 | 一覧画面に検索機能を追加 | alice | https://linear.app/*****/issue/A-1446 | | #242 | ログイン画面のバグを修正 | bob | https://linear.app/*****/issue/A-1501 |
Step 3: プロジェクトごとのグループ化
前回の step 2 で生成した Pull Request 一覧をもとにそれぞれに紐づく Linear チケット ID を抽出し、チケットからプロジェクト情報を取得して、プロジェクト単位に整理された Pull Request 一覧 を作成します。
1. Parse the Markdown table from `.results/step2_result_release_diff_prs.md` generated in STEP1
2. For each PR row, extract the ticket ID from the Linear URL column
(Format: `https://linear.app/*****/issue/A-XXXX/` or `B-XXXX/`)
3. For each ticket ID found, use Linear MCP's `get_issue` tool to fetch the issue and get its project information
- If an error occurs (ticket not found, project information cannot be retrieved, etc.), treat that PR's project as "-"
4. Update the Linear Project column in the Markdown table with the fetched project names
- Use "-" for PRs without a ticket ID or when project is not found
5. Execute the following script to group PRs by project:
`./scripts/group_prs_by_project.sh ./results/step2_result_release_diff_prs.md ./results/step3_result_prs_by_project.md`
- This script groups PRs by project, sorts them (with "No project" at the end), and splits into separate markdown tables per project
- The Linear Project column is removed from the output tables (set to "-")
6. Verify the generated file:
`./results/step3_result_prs_by_project.md`
ここでも Step 1 と同じように、プロジェクトごとのグループ化処理を安定的に実行するため、スクリプトを切り出しています。
#!/usr/bin/env bash
set -euo pipefail
INPUT_FILE="${1:?Input file required}"
OUTPUT_FILE="${2:-grouped_prs.md}"
# 1. プロジェクト名でソートして一時抽出(Markdown の表から必要な列を抜き出す)
# 第6カラム(Project 名)をソートキーとして先頭に付与
data=$(grep "^|" "$INPUT_FILE" | grep -vE "^\| (---|PR)" | awk -F'|' '{
project = ($6 ~ /^[[:space:]]*-?[[:space:]]*$/) ? "No project" : $6;
gsub(/^[[:space:]]+|[[:space:]]+$/, "", project);
print project "\t" $0
}' | sort -k1,1)
# 2. グルーピングして出力
echo "# PRs Grouped by Project" > "$OUTPUT_FILE"
current_project=""
echo "$data" | while IFS=$'\t' read -r project row; do
if [[ "$project" != "$current_project" ]]; then
echo -e "\n## ${project}\n\n| PR | Title | Author | Linear URL |" >> "$OUTPUT_FILE"
echo "| --- | --- | --- | --- |" >> "$OUTPUT_FILE"
current_project="$project"
fi
echo "$row" | awk -F'|' '{print "|" $2 "|" $3 "|" $4 "|" $5 "|"}' >> "$OUTPUT_FILE"
done
echo "Saved to: $OUTPUT_FILE"
上記のプロンプトから、Linear のプロジェクトが考慮されグループ化された出力が得られます。
検索機能追加 プロジェクト | PR | Title | Author | Linear URL | | --- | --- | --- | --- | | #237 | 一覧画面に検索機能を追加 | alice | https://linear.app/*****/issue/A-1446 | ログインフローの改修 プロジェクト | PR | Title | Author | Linear URL | | --- | --- | --- | --- | | #242 | ログイン画面のバグを修正 | bob | https://linear.app/upsider/*****/A-1501 |
Step 4: システム変更の作成
Step 3 で生成された プロジェクト単位に整理された Pull Request 一覧 をもとに、ユーザーの選択(プロジェクト / Pull Request / チケット/範囲)に応じて、Why / Whatを生成しNotion にシステム変更を作成します。選択方法は、プロジェクト名(例:問い合わせフォーム)、Pull Request 番号(例:#237)、チケット ID(例:A-1446)、範囲指定(例:2-11)などを受け取るように指示しています。
選択された Pull Request に対して、以下の処理を行います。
Why/What の自動生成
Pull Request のタイトル、説明文、コミット履歴、および関連する Linear チケット情報を分析し、Why と What を自動生成します。Notion テンプレートの適用
Notion 上のシステム変更テンプレートを取得し、自動生成した Why/What の内容を埋め込みます。Notion ページの作成
生成した内容をもとに、システム変更データベースに新しいページを作成します。 ページには、タイトル、Why、What、関連する Linear チケットへのリンクなどが含まれます。
これらをプロンプトに起こすと以下のような形になります。
1. Stop here first. To create Notion pages for selected PRs from the list, read the contents of `./results/step2_result_prs_by_project.md` and display them in a numbered list format
- Display format: Display each PR in the format `[number]. [PR #number] [PR title] - [project name]`
- Display in separate sections by project
2. Ask the user which PRs to create pages for
3. Based on the response, use Notion MCP to copy the template content and create a new page (use `create-pages` instead of `duplicate-page`)
- Response interpretation:
- Project name (e.g., "Inquiry form", "Riverpod 3.0") → Create one page combining all PRs under that project
- PR number (e.g., "#237", "237", "PR #237") → Create one page per PR
- Ticket ID (e.g., "A-1446") → Create one page combining all PRs linked to that ticket
- Range specification (e.g., "2-11") → Target PRs from number 2 to 11 in the list
- Comma-separated (e.g., "237, 242" or "Inquiry form, A-1446") → Multiple specifications
- Combine multiple PRs into one page (e.g., "217, 248, 289 as one") → Combine specified PRs into one page, with a title based on a common theme (e.g., "Update dependencies")
- Example: "All renovate PRs with No project together"
- **Title generation**:
- Generate a descriptive title that summarizes the content of PRs, tickets, and projects
- The title must not include PR numbers (e.g., "#237") or ticket IDs (e.g., "A-1446")
- Use the project name as-is if it's a project name
- **Why/What generation method**:
1. Get information about the target PRs (from GitHub API or existing Markdown files)
2. Analyze PR titles, descriptions, commit history, and related Linear ticket information
3. Why: Summarize the purpose, background, and benefits for users (max 5 lines)
4. What: Summarize implementation details in bullet points (max 30 characters per item, max 10 items)
- **Template handling**:
1. Fetch the template from the following URL and get the latest content:
https://www.notion.so/*****/SystemChange-Template
- Use Notion MCP's `notion-fetch` tool
2. Extract the `content` field from the fetched template (Notion-flavored Markdown format)
3. Copy all sections, callouts, and checkboxes from the template without omission
4. Dynamically replace the following placeholders:
- `{{Why}}` → Replace with the generated "Why" content (in Notion-flavored Markdown format)
- `{{What}}` → Replace with the generated "What" content (in Notion-flavored Markdown format)
4. Save the title and URL of the created Notion page to the following file:
- File path: `./results/step3_result_system_changes.md`
- Save format: Record in the format `[Title](URL)` on each line (one line per page)
- Create a new file if it doesn't exist, or append to the end if it exists
こちらを実行すると step3_result_system_changes.md として以下のような出力を得られます。
Ticket: A-1446 一覧画面に検索機能を追加 Why: - 一覧から目的のアイテムを探すのに時間がかかっており、 検索機能を追加することでユーザーが素早く目的の情報に到達できるようにするため。 What: - 一覧画面の上部に検索バーを追加 - タイトルと説明文を対象に部分一致検索を実装 - 検索キーワード入力中はリアルタイムに結果を絞り込み表示
Step 5: 生成結果の表示
前の step で生成されたシステム変更ページの URL を一覧表示します。これにより、システム変更をレビュワーなどにすぐに展開することが可能となります。
1. Read the contents of `./results/step3_result_prs_by_project.md`
2. Read the System Changes page URLs corresponding to each PR from `./results/step4_result_system_changes.md`
- File format: Assume each line is recorded in the format `[Title](URL)` or `Title: URL`
- Map the correspondence between PRs and System Changes using PR numbers or ticket IDs
3. Create a Notion page (use `create-pages`)
- Page title: "Release PR List - {BASE_BRANCH} to {TARGET_BRANCH}" (use values obtained in STEP2)
- content: Create a table in Notion-flavored Markdown format
- Separate sections by project (`## [Project Name]`)
- Place a Markdown table in each section
- Table columns:
- PR: Format `[PR #number](PR URL)`
- Title: Format `[PR Title](PR URL)`
- Author: PR creator
- Linear URL: Linear ticket URL (use "-" if not available)
- System Changes: Notion page URL created in STEP4 (use "-" if not available)
- Display URL in the format `[System Change Title](URL)`
4. Keep the created Notion page URL
5. Open the browser with `open <URL>`</span>
この一連の流れにより、リリース準備の作業時間を大幅に短縮しながら、必要な情報を網羅的に記録できます。
まとめ
リリース時のシステム変更管理や承認作業は、金融系サービスにおいて安全性を担保するために欠かせない重要なプロセスです。 一方で、人手で行うとどうしてもコストがかかり、開発者の負担になりがちです。
今回の自動化によって、
- リリース準備にかかる工数の削減
- PRやチケット情報の自動集約による記載漏れの防止
- 開発者が本来の開発に集中できる環境の改善
といった効果を得ることができました。
また、実際に運用を開始したことで、いくつか新たな課題も見えてきました。
- AIによる生成内容のばらつき
- Notion MCPを介したページ作成の失敗
自動化は一度作って終わりではありません。今後も安全性を担保しながら、開発と運用の両立を支える仕組みづくりを続けていきたいと考えています。
We Are Hiring
UPSIDER Engineering Deckはこちら📣