Google Apps Script Web App|GitHub Actions連携で無料CI/CD打包ツール構築|跨團隊共有を実現
開発チーム向けにGAS Web AppでGitHub、Slack、Firebase、Asana/Jira APIを連携し、中継プラットフォームを構築。打包作業を自動化し、効率的なCI/CD環境を無料で実現します。
本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。
記事一覧
CI/CD 実践ガイド(4):Google Apps Script Web App を使って GitHub Actions と連携し、無料で使いやすいパッケージツールプラットフォームを構築する
GAS Web App で GitHub、Slack、Firebase、または Asana/Jira API と連携し、中継サーバーを構築してチーム間で共有できるパッケージングツールプラットフォームを提供する
Photo by Lee Campbell
前書き
前回の「CI/CD 実践ガイド(三):GitHub Actions を使った App iOS CI と CD ワークフローの実装」では、App iOS プロジェクトの CI/CD 基盤機能を完成させました。これにより、CI の自動テスト検証と CD のビルド・デプロイが可能になりました。しかし、実際のプロダクト開発プロセスでは、ビルド・デプロイ作業は主に他の職能パートナーに引き渡して QA(品質保証)機能検証を行うためのものです。このため、CD の利用シーンはエンジニアだけに限らず、QA、PM、デザイン(Design QA)、さらには経営者が先に試したい場合などにも及びます。
GitHub Actions の workflow_dispatch 手動フォームトリガーは、簡単なフォームを提供してユーザーがビルド操作を行えますが、対象が非エンジニアの場合は非常に使いづらいです。彼らは「ブランチとは何か?」「項目は入力すべきか?」「ビルドが完了したかどうかはどう確認する?」「完了後はどうやってダウンロードするのか?」など分かりません。
また、権限管理の問題もあります。別の職能メンバーに直接 GitHub Actions でビルドさせる場合、そのメンバーのアカウントをリポジトリに追加しなければなりません。セキュリティ管理上非常に危険かつ不合理であり、単にビルドフォームを操作するだけなのにソースコード全体を見せる必要があります。
Jenkinsとは異なり、GitHub Actionsには独立したWebツールプラットフォームがありません。
workflow_dispatch のフォームスタイル
したがって、他の職能ユーザーにサービスを提供するための中継ステーションとしてのパッケージングプラットフォームが必要です。Asana/Jiraのタスクを統合し、ユーザーがタスクから直接アプリをパッケージングでき、進捗確認やパッケージ結果のダウンロードもその場で行えます。
GAS Web App 中継ステーション
上一篇 は右側のコアである GitHub Actions CI/CD ワークフローの開発に焦点を当てています。この記事は左側のエンドユーザー向けのパッケージングツールプラットフォームとユーザー体験の向上に注目しています。
Google Apps Script — Web App パッケージングツールプラットフォーム 成果図
パッケージフォーム: プロジェクト管理ツールからチケット番号を取得し、GitHubからオープン中のプルリクエストを取得する連携
パッケージ記録: パッケージの履歴を記録し、進行中のパッケージタスクの進捗状況を表示。クリックで Firebase App Distribution のダウンロードリンクを取得し、情報を表示
Runner 状態: Self-hosted Runner の状態を表示
Slack へのパッケージ進行状況通知
モバイル対応
組織チーム内のアカウント使用制限をサポート
主な職務内容
0 ステータス、0 データベース、単純な中継交換所として、各種 API のデータ(例:Asana/Jira/GitHub)を統合表示し、フォームリクエストを GitHub Actions に転送します。
操作要件: モバイルとPCに対応。
権限要件: チーム組織のメンバーのみアクセス可能に制限できること。
オンラインデモ Web アプリ
- 初めてご利用の場合は、下の図の認証を参照してください(デモアプリ専用):
プロジェクトソースコード: https://script.google.com/home/projects/1CBB39OMedqP9Ro1WSlvgDnMBin4-ksyhgly2h_KrbOuFiPHTalNgwHOp/edit
技術選択
第一篇記事で触れましたが、ここで改めて詳しくまとめます。
Slack への統合
私たちは Slack をパッケージングプラットフォームとして試み、自前でバックエンドサービスを開発し GCP 上に構築しました。Slack API、Asana API と連携し、Slack から送られたフォームを GitHub API に転送して GitHub Actions をトリガーする仕組みです。体験としては非常に快適でチームのコラボレーションツールに統一でき、スムーズに使えました。しかし、欠点は開発およびその後の保守コストが非常に高いことです。Ktor で開発したバックエンドサービスのため、アプリエンジニアがバックエンドも兼任し、Google サービスの OAuth 統合問題を処理し、一部機能(例えば審査送信など)もここで実装する必要があり、機能は複雑でした。後から入った新人が引き継げなければほぼ保守不可能になり、さらに毎月 $15 USD の GCP サーバー費用もかかります。
初期は Cloud Functions などの FaaS サービスを使って Slack API と連携しようとしましたが、Slack API は3秒以内に応答しないと失敗とみなされます。FaaS にはコールドスタート問題があり、一定期間呼び出されないと休止状態になり、再度呼び出すと応答に長時間(≥ 5秒)かかるため、Slack のパッケージフォームが非常に不安定になり、頻繁にタイムアウトエラーが発生しました。
内部システムへの統合
これはもちろん最適解であり、チームにWebやバックエンドの人材がいる場合は、既存のシステムと直接統合するのが最も良く、かつ安全です。
この記事の前提は:なし、アプリは自立自強。
Google Apps Script — Web アプリ
Google Apps Script は私たちの古くからのパートナーで、これまで多くの RPA プロジェクトでスケジュールトリガーによるタスク実行に使ってきました。例えば、「Crashlytics + Google Analytics 自動で App Crash-Free Users Rate を取得」や「Google Apps Script を使った毎日データレポートの RPA 自動化」などです。その時にまた思い出したのが、GAS には Web (App) としてデプロイして直接ウェブサービスにできる機能があることです。
Google Apps Script の利点:
✅ 無料で、通常の使用範囲ではほぼ上限に達しない
✅ Functions as a Service(ファンクションズ・アズ・ア・サービス)方式で、自分でサーバーを構築・維持する必要がありません
✅ 権限管理は Google Workspace と同様で、組織内の Google アカウントのみ使用可能に設定できます
✅ Googleエコシステム関連サービス(例:Firebase、GAなど)やデータとの無痛統合(OAuth不要)
✅ プログラミング言語は JavaScript を使用し、習得が容易(V8 Runtime は ES6+ をサポート)
✅ 迅速な作成、迅速な公開、迅速な利用
✅ サービスは安定しており、長期間(16年以上)提供されています
✅ AIが助けになる!ChatGPTを活用した開発で、正確率は95%に達することを実証
Google Apps Script の欠点:
❌ 内蔵のバージョン管理は一言で説明できません
❌ ファイル、データ保存、キー/証明書管理は標準でサポートされていません
❌ Web App は100%のレスポンシブデザイン(RWD)体験を実現できません
❌ プロジェクトは個人アカウントにのみ紐付け可能で、組織には紐付けられません
❌ Googleは継続的に開発と保守を行っていますが、全体の機能更新は遅いです
❌ ネットワークリクエスト
UrlFetchAppはUser-Agentの設定をサポートしていません❌ Web App の
doGet/doPostではHeaders情報の取得がサポートされていません❌ FaaS コールドスタート問題
❌ 複数人同時に開発することはサポートしていません
しかし、Web Appではあまり影響がなく、せいぜい数秒待ってページに入るだけです。
以上は GAS 自体のサービスの長所短所ですが、パッケージングツールの Web に与える影響はあまり大きくありません。この方法を Slack 方式と比較して選ぶと、より速く、軽量で、引き継ぎやすいという利点があります。欠点はチームがこのツールの URL と使い方をよく知っている必要があり、また GAS のライブラリ機能が限られていること(例:組み込みの暗号化アルゴリズムライブラリがない)ため、基本的に純粋な中継プラットフォームしか作れません。例えば審査申請を送る場合も、GitHub Actions に送信リクエストを転送するだけになります。
また、Google Workspace の作業環境のチームのみ対象です; リソースとニーズを考慮し、Google Apps Script — Web App を使ってパッケージングツールプラットフォームを構築しました。
UI フレームワーク
私たちは直接BootstrapのCDNを使用します。自分でCSSスタイルを作るのは面倒なので、BootstrapをAIにどう組み合わせて使うか尋ねる方が正確で便利です。
実践してみよう
こちらでプラットフォーム全体の構成をオープンソース化しましたので、各チームは自分たちのニーズに合わせてこのバージョンをカスタマイズしてください。
オープンソースサンプルプロジェクト
GAS 上で直接プロジェクトを表示:
GitHub リポジトリのバックアップ:
ファイル構成
とてもシンプルなクラスベースのMVC風アーキテクチャを書きました。調整したい場合や機能がわからないときは、AIに尋ねれば正確な答えが得られます。
システム
appsscript.json: GAS システムのメタデータ設定ファイル
ポイントは「oauthScopes」変数で、このスクリプトが使用する外部権限を宣言します。Entrypoint.gs: doGet( ) エントリーポイントを定義する
コントローラー
- Controller_iOS.gs: iOSパッケージツールページのコントローラーで、Viewに表示するためのデータ取得を担当します
ビュー
View_index.html: パッケージングツールの全体構造とホームページ
View_iOS.html: iOS パッケージングツールページの骨組み
View_iOS_Runs.html: iOS パッケージングツール — パッケージ履歴内容ページ
View_iOS_Form.html: iOS パッケージングツール — パッケージングフォームページ
View_iOS_Runners.html: iOS ビルドツール — セルフホステッドランナーのステータスページ
モデル(ライブラリ)
Credentials.gs: キー内容を定義
(⚠️️️ご注意️、GASでGCP IAMを使うのはかなり複雑なため、ここで直接キーを定義しています。そのため、このGASプロジェクトには機密情報が含まれているので、プロジェクトの閲覧や編集権限を安易に共有しないでください )StubData.gs: Online Demo 用のスタブメソッドとデータ。
Settings.gs: 一部の共通設定と lib の初期化。
GitHub.gs: GitHub API 操作のラップ。
Slack.gs: Slack API 操作のラップ。
Firebase.gs: Firebase — App Distribution API 操作のラッパー。
自分のパッケージングプラットフォームを作成する
- Google Apps Script プロジェクト を作成し、名前を付ける
プロジェクト設定 → 「エディタで『appsscript.json』マニフェストファイルを表示する」にチェックを入れると、「appsscript.json」メタデータファイルが表示されます。
2.参照私のオープンソースプロジェクトファイル、すべてのファイルを例に従って作成し、内容をそのままコピーしてください。
馬鹿げているが、仕方がない。
StubData.gs はまず一緒にコピーしてください。初回デプロイのテストで使用できます。
もう一つの方法は、clasp (Google Apps Script CLI) を使ってデモプロジェクトを git clone し、その後コードをプッシュすることです。
コピーが完了すると、サンプルプロジェクトと全く同じになります。
- 初回デプロイ「ウェブアプリケーション」で結果を確認する
プロジェクト右上の「デプロイ」→「新しいデプロイを追加」→ タイプ「ウェブアプリケーション」:
実行権限:
私は「すべてあなたのアカウントでスクリプトを実行します。」
ウェブアプリの利用者は、現在ログインしている Google アカウントのユーザーとしてスクリプトを実行します。
誰がアクセスできますか:
私だけです
XXX 同じ組織内のすべてのユーザー
同じ組織かつログイン済みのGoogleアカウントユーザーのみアクセス可能。ログイン済みの Google アカウントユーザーはすべてアクセス可能です。
全員が
Googleアカウントにログインする必要はなく、誰でも公開アクセス可能です。
内部ツールの場合:「アクセス可能なユーザー:XXX 同じ組織内のすべてのユーザー」+「実行者の身分:ウェブアプリにアクセスするユーザー」を選択してセキュリティ管理が可能です。
デプロイ完了後の「ウェブアプリケーション」URLが、あなたのWeb AppパッケージツールのURLになります。チームメンバーと共有して使えます。(URLは見栄えが悪いので、短縮URLサービスで加工しても良いです。デプロイ内容の更新ではURLは変わりません)
ユーザーが初めて使用する際は許可を同意する必要があります
初めて Web App のURLをクリックすると、最初に認証の同意が必要です。
Review Permission → この Web アプリを使用するアカウントの権限を選択してください
未検証の警告ウィンドウで、「詳細」をクリックして展開 → 「XXX」へ移動(安全ではありません)をクリック
- 「許可する」をクリックしてください
以降、スクリプトの権限が変更されない限り、再認証は不要です。
認証同意が完了すると、パッケージングツールのホームページに移動します:
Demo パッケージツールのデプロイ成功 🎉🎉🎉
注:「このアプリケーションは Google Apps Script のユーザーによって作成されました」というメッセージは自動で非表示にできません。
デプロイの更新
⚠️すべてのコード変更は、デプロイの更新後に反映されます。
️️⚠️すべてのコードの変更は、デプロイの更新後に有効になります。
⚠️すべてのコード変更は、デプロイを更新しないと反映されません。
ここで注意すべきは、コードの変更を保存しても Web App に直接反映されないため、リロードしても変化がない場合はこのためです;「デプロイ」→「デプロイの管理」→「編集」→ バージョン「新しいバージョンを作成」→ 「デプロイ」→ 「完了」 の操作が必要です。
デプロイ完了後にページをリロードすると変更が反映されます。
テストデプロイを追加して開発を便利にする
前述の通り、すべての変更を反映させるにはデプロイの更新が必要です。これは開発段階では非常に面倒なので、開発中は「テストデプロイ」を使って変更が正しいか素早く検証できます。
「デプロイ」→「テストデプロイ作業」→ テスト用「ウェブアプリケーション」URLを取得。
開発段階では、このURLを直接使用して保存できます。ファイルの変更を保存したら、この開発用URLに戻ってページをリロードすると成果が確認できます!
すべての開発が完了したら、前述の手順に従って更新とデプロイを行い、ユーザーにリリースします。
Demoサンプルプロジェクトを実際のデータに接続する方法
次に本題です。実際のデータ連携として、GitHub Actions Workflow は前回の記事で作成した CI/CD フローを参照しています。実際の Actions Workflow に合わせてパラメータを調整してください。
⚠️変更前にご注意ください
Google Apps Script プラットフォームは複数人や複数ウィンドウでの開発をうまくサポートしていません。私が経験した問題は、誤って2つの編集ウィンドウを開き、Aで編集した後にBで編集したため、変更がBの古いバージョンで上書きされてしまったことです。したがって、同時に編集するのは1人1ウィンドウのみを推奨します。
GitHub 連携
GitHub API トークンの設定:
GitHub -> アカウント -> 設定 -> Developer Settings -> Fine-grained personal access tokens または Personal access tokens (classic)。
Fine-grained personal access tokens を使用することをお勧めします(安全ですが、有効期限があります)。
Fine-grained personal access tokens に必要な権限は以下の通りです:
Repo: 操作するリポジトリを選択してください
権限:
Actions (読み取り/書き込み)、Administration (読み取り専用)
もし特定の個人アカウントに依存したくない場合は、クリーンなチーム用の GitHub アカウントを作成し、そのトークンを使用することをお勧めします。
GAS プロジェクト → Credentials.gs → githubToken 変数にトークンを入力してください。
GithubStub を GitHub に置き換える:
到 GAS プロジェクト → Settings.gs → 次のように設定:
1
const iOSGitHub = new GitHubStub(githubToken, iOSRepoPath);
変更して
1
const iOSGitHub = new GitHub(githubToken, iOSRepoPath);
ファイルを保存する。
テスト用「ウェブアプリケーション」URLをリロードして変更が正しいか確認する:
正しくデータが表示されているということは: GitHubが本物のデータに正常に接続できた 🎉🎉🎉
「Runner 状態」に切り替えて、Self-hosted Runner の状態が正常に取得できているか確認できます:
注:私のランナーは起動していないため、オフライン状態です。
Slack 連携
Slack通知を連携するために、まずリポジトリのGitHub Actionsに戻り、パッケージビルドActionのワークフロー通知用コンテナActionを新規作成します。
CD-Deploy-Form.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# ワークフロー(Action)名
name: CD-Deploy-Form
# Actions ログのタイトル名
run-name: "[CD-Deploy-Form] ${{ github.ref }}"
# 同じ Concurrency Group 内で新しいジョブがあれば実行中のジョブをキャンセル
# 例:同じブランチのビルドジョブが重複トリガーされた場合、前のジョブをキャンセル
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# トリガーイベント
on:
# 手動フォームトリガー
workflow_dispatch:
# フォーム入力フィールド
inputs:
# アプリのバージョン番号
VERSION_NUMBER:
description: 'アプリのバージョン番号(例:1.0.0)。空欄の場合はXcodeプロジェクトから自動検出。'
required: false
type: string
# アプリのビルド番号
BUILD_NUMBER:
description: 'アプリのビルド番号(例:1)。空欄の場合はタイムスタンプを使用。'
required: false
type: string
# アプリのリリースノート
RELEASE_NOTE:
description: 'デプロイのリリースノート。'
required: false
type: string
# トリガーしたユーザーのSlackユーザーID
SLACK_USER_ID:
description: 'SlackのユーザーID。'
required: true
type: string
# トリガーしたユーザーのメールアドレス
AUTHOR:
description: 'トリガーしたユーザーのメールアドレス。'
required: true
type: string
# ジョブ定義
jobs:
# ビルド開始時にSlackへメッセージ送信
# ジョブID
start-message:
# GitHubホストランナーで実行、小規模利用
runs-on: ubuntu-latest
# 最大タイムアウト設定、異常時の無限待機防止
# 通常は5分以上かからない
timeout-minutes: 5
# ジョブステップ
steps:
- name: Slackに開始メッセージを投稿
id: slack
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ inputs.SLACK_USER_ID }}
text: "ビルドリクエストを受け取りました。\nID: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\\|${{ github.run_id }}>\nBranch: ${{ github.ref_name }}\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"
# ジョブ出力、後続ジョブで使用
# ts = SlackメッセージID、後続通知で同スレッドに返信可能
outputs:
ts: ${{ steps.slack.outputs.ts }}
deploy:
# ジョブはデフォルト並列実行、needsでstart-message完了待ちに設定
# ビルド・デプロイ実行ジョブ
needs: start-message
uses: ./.github/workflows/CD-Deploy.yml
secrets: inherit
with:
VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }}
BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }}
RELEASE_NOTE: ${{ inputs.RELEASE_NOTE }}
AUTHOR: ${{ inputs.AUTHOR }}
# ビルド・デプロイ成功時のメッセージ
end-message-success:
needs: [start-message, deploy]
if: ${{ needs.deploy.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Slackに成功メッセージを投稿
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ inputs.SLACK_USER_ID }}
thread_ts: "${{ needs.start-message.outputs.ts }}"
text: "✅ ビルド・デプロイ成功しました。\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"
# ビルド・デプロイ失敗時のメッセージ
end-message-failure:
needs: [deploy, start-message]
if: ${{ needs.deploy.result == 'failure' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Slackに失敗メッセージを投稿
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ inputs.SLACK_USER_ID }}
thread_ts: "${{ needs.start-message.outputs.ts }}"
text: "❌ ビルド・デプロイ失敗しました。実行結果を確認するか、後ほど再試行してください。\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"
# ビルド・デプロイキャンセル時のメッセージ
end-message-cancelled:
needs: [deploy, start-message]
if: ${{ needs.deploy.result == 'cancelled' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Slackにキャンセルメッセージを投稿
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ inputs.SLACK_USER_ID }}
thread_ts: "${{ needs.start-message.outputs.ts }}"
text: ":black_square_for_stop: ビルド・デプロイがキャンセルされました。\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"
完全なコード: CD-Deploy-Form.yml
この Action は単なるコンテナで、Slack 通知と連携しています。実際には、前回の記事で作成した CD-Deploy.yml Action を再利用しています。
Slack Bot App の作成とメッセージ送信権限の設定については、私の以前の記事をご参照ください。
Repo → Secrets に対応する
SLACK_BOT_TOKENを追加し、Slack Bot App のトークン値を設定するのを忘れないでください。
GAS プロジェクトに戻り → Credentials.gs → slackBotToken 変数にトークンを入力します。
再び GAS プロジェクト → Settings.gs → 次のように変更してください:
1
const slack = new SlackStub(slackBotToken);
変更して
1
const slack = new Slack(slackBotToken);
ファイルを保存する。
既存の Slack Bot App がなく通知を送るのが面倒な場合は、ここでのすべての手順を無視し、GAS プロジェクト内の slack に関する使用部分を削除してください。
GitHub 連携 — パッケージフォーム
GAS プロジェクト → Controller_iOS.gs → View_iOS_Form.html の内容を調整:
仮の Asana タスク連携方法を削除:
<? tasks.forEach(function(task) { ?>
<option value="<?=task.githubBranch?>">[<?=task.id?>] <?=task.title?></option>
<? }) ?>
ここでデフォルトブランチ(ここでは main )を自分で調整することもできます。
—
GAS プロジェクト → Controller_iOS.gs → iOSLoadForm() の内容を調整:
template.tasks = Stubable.fetchStubAsanaTasks();の Asana のモック接続メソッドを削除してください。
Asana/Jira と連携する場合は、直接 ChatGPT に連携方法を尋ねてください。template.prs = iOSGitHub.fetchOpenPRs();は実際に GitHub API を使ってオープン中の PR リストを取得します。必要に応じて残してください。
送信後の処理 iOSSubmitForm() の内容:
実際の GitHub Actions Workflow ファイル名や workflow_dispatch の inputs パラメータに応じて調整してください:
1
2
3
4
5
6
7
8
iOSGitHub.dispatchWorkflow("CD-Deploy-Form.yml", branch, {
"BUILD_NUMBER": buildNumber,
"VERSION_NUMBER": versionNumber,
"VERSION_NUMBER": versionNumber,
"RELEASE_NOTE": releaseNote,
"AUTHOR": email,
"SLACK_USER_ID": slack.fetchUserID(email)
});
必須項目のバリデーションも追加可能で、ここではブランチの入力が必須であることだけを検証し、入力がないとエラーメッセージが表示されます。
もしこれだけでは安全性が不十分だと感じる場合は、自分でパスワード認証を追加したり、特定のアカウントのみ使用可能にすることもできます。
最後の行 Slack通知機能はSlackの設定が必要です。Slack Bot Appがない場合やSlack連携をしたくない場合は、Demo Actions Repo で
iOSGitHub.dispatchWorkflow("CD-Deploy.yml")に変更し、SLACK_USER_IDパラメータを削除してください。
テスト用の「ウェブアプリケーション」URLをリロードして変更が正しいか確認する:
パッケージングフォームには「Opened PR List」だけが残っています。
データを入力して「リクエスト送信」を押し、ビルドフォームをテストしてください:
送信が成功したということは問題ないことを意味し、パッケージ記録に戻るとタスクが開始されているのも確認できます 🎉 :
重複したビルド記録は進捗を更新可能です。
よくある送信エラー:
Required input ‘SLACK_USER_ID’ not provided: GitHub Actions のこの SLACK_USER_ID フィールドは必須ですが、渡されていません。Slack の設定に失敗しているか、現在のユーザーのメールアドレスに対応する Slack UID が見つからない可能性があります。
Workflow does not have ‘workflow_dispatch’ trigger、分支が古いため、xxx ブランチを更新してください:選択したブランチに対応する Action Workflow ファイル(iOSGitHub.dispatchWorkflow で指定されたファイル)が見つかりません。
No ref found for、找不到分支: このブランチが見つかりません。
Firebase App Distribution — ダウンロードリンクの取得と連携
最後の小機能は Firebase App Distribution と連携し、直接ダウンロード情報とリンクを取得できるようにすることで、スマホで打包プラットフォームツールを開いてクリックするだけで直接ダウンロード・インストールが可能になります。
以前に「Google Apps Script x Google APIs 簡単連携方法」で紹介したように、GASはFirebaseと迅速かつスムーズに連携できます。
接続の原理
連携を始める前に、この「Tricky」の連携の仕組みについて説明します。
私たちのパッケージングプラットフォームにはデータベースがなく、純粋にAPIの中継ステーションとして機能しています。そのため、実際にはGitHub Actions CD-Deploy.ymlでパッケージング作業を行う際に、Job Run IDをリリースノートに渡しています(もちろんBuild Numberに渡すことも可能です):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ID="${{ github.run_id }}" // ジョブ実行ID
COMMIT_SHA="${{ github.sha }}"
BRANCH_NAME="${{ github.ref_name }}"
AUTHOR="${{ env.AUTHOR }}"
# リリースノートを組み立てる
RELEASE_NOTE="${{ env.RELEASE_NOTE }}
ID: ${ID}
Commit SHA: ${COMMIT_SHA}
Branch: ${BRANCH_NAME}
Author: ${AUTHOR}
"
# Fastlaneでビルド&デプロイのレーンを実行
bundle exec fastlane beta release_notes:"${RELEASE_NOTE}" version_number:"${VERSION_NUMBER}" build_number:"${BUILD_NUMBER}"
これで Firebase App Distribution のリリースノートに Job Run ID が表示されます。
GAS Web App パッケージングツールプラットフォームは GitHub API と連携して GitHub Actions の実行履歴を取得し、API から得た Job Run ID を使って Firebase App Distribution API でリリースノートを検索します。リリースノートに *ID: XXX* が含まれるバージョンを見つけることで、対応するパッケージング記録を特定できます。
データベースを使用せずに2つのツールプラットフォームの対応が可能。
プロジェクト設定の連携
GAS → プロジェクト設定 → Google Cloud Platform (GCP) プロジェクト → プロジェクトを変更:
接続したい Firebase プロジェクトの番号を入力してください。
初回設定時にエラーが発生する場合があります 「プロジェクトを変更するには、OAuth同意画面を設定してください。OAuth同意画面の詳細設定。」と表示された場合、以下の手順はスキップしても問題ありません。
「OAuth 同意画面の詳細」リンクをクリック → 「同意画面の設定」をクリック:
「開始」をクリック:
アプリケーション情報:
アプリケーション名:
あなたのツール名を入力してくださいユーザーサポートメール:
選択したメール
対象者:
内部:組織内のメンバーのみ使用可能
外部:すべての Google アカウントユーザーが同意し認可後に使用可能
連絡先情報:
- 通知を受け取るメールアドレスを入力してください
《 Google API サービス:ユーザーデータポリシー 》に同意します。
最後に「 作成 」をクリックします。
—
GAS に戻り → プロジェクト設定 → Google Cloud Platform (GCP) プロジェクト → プロジェクトを変更:
Firebase プロジェクト番号を再入力し、「プロジェクトを変更」をクリックします。
エラーが表示されなければ、バインディングは完了です。
—
「外部」を選択した場合、以下の設定も必要になることがあります:
「プロジェクト番号」をクリック → 左側メニューを展開 → 「API とサービス」→ 「OAuth 同意画面」
「対象を選択」→ テスト 「アプリを公開」をクリック → 完了。
ユーザーは前述の「ユーザー初回利用時に同意が必要」の手順に従って認証を完了すれば、すぐに利用可能です!
もし上記の手順が設定されていない場合、ユーザーは以下のエラーに遭遇します:
アクセス権「XXX」はGoogle認証手続きを完了していません。
— — —
プロジェクト連携
接続に戻ると、Firebase は ScriptApp.getOAuthToken() を使って実行ユーザーの権限に応じて動的にトークンを取得するため、トークンの設定は不要です。
GAS プロジェクト → Settings.gs → に移動して、以下を設定してください:
1
const iOSFirebase = new FirebaseStub(iOSFirebaseProject);
変更する
1
const iOSFirebase = new Firebase(iOSFirebaseProject);
すぐに可能です。
テスト用「ウェブアプリケーション」URLを打包記録にリロード → 記録の一つを選んで「ダウンロードリンクを取得」をクリック:
Firebase App Distribution のリリースノートで対応する Job Run Id のビルドが見つかった場合、ダウンロード情報が直接表示され、ダウンロードをクリックすると直接ダウンロードページに移動します。
完了!🎉🎉🎉
成果
ここまでで、サンプルはすべて実際に使えるパッケージングツールに変更済みです。残りのカスタマイズ機能や、さらに多くのサードパーティAPI連携、追加のフォームはご自身で拡張可能です(ChatGPTと相談しながら)。
最後に、開発とテストが完了したら必ず前述の手順に従い — デプロイの更新を行わないと反映されません!
連携拡張
「中継ステーション」役割の精神を受け継ぎ、ここにいくつかの迅速な連携用チートシートを提供します:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function asanaAPI(endPoint, method = "GET", data = null) {
var options = {
"method" : method,
"headers": {
"Authorization": "Bearer "+asanaToken
},
"payload" : data
};
var url = "https://app.asana.com/api/1.0"+endPoint;
var res = UrlFetchApp.fetch(url, options);
var data = JSON.parse(res.getContentText());
return data;
}
asanaAPI("/projects/{project_gid}/tasks")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// jql = フィルター条件
function jiraTickets(jql) {
const url = `https://xxx.atlassian.net/rest/api/3/search`;
const maxResults = 100;
let allIssues = [];
let startAt = 0;
let total = 0;
do {
const queryParams = {
jql: jql,
startAt: startAt,
maxResults: maxResults,
fields: "assignee,summary,status"
};
const queryString = Object.keys(queryParams)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
.join("&");
const options = {
method: "get",
headers: {
Authorization: "Basic " + jiraToken,
"Content-Type": "application/json",
},
muteHttpExceptions: true,
};
const response = UrlFetchApp.fetch(`${url}?${queryString}`, options);
const json = JSON.parse(response.getContentText());
if (response.getResponseCode() != 200) {
throw new Error("Jiraの課題取得に失敗しました。");
}
if (json.issues && json.issues.length > 0) {
allIssues = allIssues.concat(json.issues);
total = json.total;
startAt += json.issues.length;
} else {
break;
}
} while (startAt < total);
var groupIssues = {};
for(var i = 0; i < allIssues.length; i++) {
const issue = allIssues[i];
if (groupIssues[issue.fields.status.name] == null) {
groupIssues[issue.fields.status.name] = [];
}
groupIssues[issue.fields.status.name].push(issue);
}
return groupIssues;
}
jiraTickets(`project IN(App)`);
もし本当にデータベースが必要な場合は、Google Sheets を代わりに使用できます:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Saveable {
constructor(type) {
// https://docs.google.com/spreadsheets/d/Sheet-ID/edit
const spreadsheet = SpreadsheetApp.openById("Sheet-ID");
this.sheet = spreadsheet.getSheetByName("Data"); // シート名
this.type = type;
}
write(key, value) {
this.sheet.appendRow([
this.type,
key,
JSON.stringify(value)
]);
}
read(key) {
const data = this.sheet.getDataRange().getValues();
const row = data.find(r => r[0] === this.type && r[1] === key);
if (row) {
return JSON.parse(row[2]);
}
return null;
}
}
let saveable = Saveable("user");
// 書き込み
saveable.write("birthday_zhgchgli", "0718");
// 読み込み
saveable.read("birthday_zhgchgli"); // -> 0718
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function slackSendMessage(channel, text = "", blocks = null) {
const content = {
channel: channel,
unfurl_links: false,
unfurl_media: false,
text: text,
blocks: blocks
};
try {
const response = slackRequest("chat.postMessage", content);
return response;
} catch (error) {
throw new Error(`Slackメッセージの送信に失敗しました: ${error}`);
}
}
function slackRequest(path, content) {
const options = {
method: "post",
contentType: "application/json",
headers: {
Authorization: `Bearer ${slackBotToken}`,
'X-Slack-No-Retry': 1
},
payload: JSON.stringify(content)
};
try {
const response = UrlFetchApp.fetch("https://slack.com/api/"+path, options);
const responseData = JSON.parse(response.getContentText());
if (responseData.ok) {
return responseData
} else {
throw new Error(`Slack: ${responseData.error}`);
}
} catch (error) {
throw error;
}
}
さらに多くの Google Apps Script の事例:
まとめ
ご覧いただき、ご参加いただき誠にありがとうございます。CI/CD 0から1シリーズの記事はここで一区切りとなります。皆様とチームのCI/CDワークフロー構築に実際に役立ち、効率と製品の安定性向上に繋がれば幸いです。実装に関するご質問があればぜひコメントでご相談ください。この4本の記事は約14日以上かけて執筆しました。もし良かったら私の Medium をフォローし、友人や同僚と共有してください。
ありがとうございます。
🍺 Buy me a beer on PayPal
本シリーズの記事は多くの時間と労力をかけて執筆しました。内容があなたやチームの作業効率や製品品質の向上に役立った場合は、ぜひコーヒーをご馳走してください。ご支援ありがとうございます!
シリーズ記事:
CI/CD 実践ガイド(二):GitHub Actions と self-hosted Runner の使い方と構築大全
CI/CD 実践ガイド(三):GitHub Actions を使ったアプリプロジェクトの CI と CD ワークフローの実装
CI/CD 実践ガイド(4):Google Apps Script Web App を使って GitHub Actions と連携し、無料で使いやすいパッケージングツールプラットフォームを構築する
ご質問やご意見がありましたら、こちらからご連絡ください 。
*Post MediumからZMediumToMarkdownによって変換されました。
本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。





















































{:target="_blank"}](/assets/4273e57e7148/1*znvPmqsaivk3KhsE26sFwA.webp)