ZhgChg.Li

Google Apps Script Web App|GitHub Actions連携で無料CI/CD打包ツール構築|跨團隊共有を実現

開発チーム向けにGAS Web AppでGitHub、Slack、Firebase、Asana/Jira APIを連携し、中継プラットフォームを構築。打包作業を自動化し、効率的なCI/CD環境を無料で実現します。

Google Apps Script Web App|GitHub Actions連携で無料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

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 のフォームスタイル`

workflow_dispatch のフォームスタイル

したがって、他の職能ユーザーにサービスを提供するための中継ステーションとしてのパッケージングプラットフォームが必要です。Asana/Jiraのタスクを統合し、ユーザーがタスクから直接アプリをパッケージングでき、進捗確認やパッケージ結果のダウンロードもその場で行えます。

GAS Web App 中継ステーション

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 操作のラッパー。

自分のパッケージングプラットフォームを作成する

  1. Google Apps Script プロジェクト を作成し、名前を付ける

プロジェクト設定 → 「エディタで『appsscript.json』マニフェストファイルを表示する」にチェックを入れると、「appsscript.json」メタデータファイルが表示されます。

2.参照私のオープンソースプロジェクトファイル、すべてのファイルを例に従って作成し、内容をそのままコピーしてください。

馬鹿げているが、仕方がない。

StubData.gs はまず一緒にコピーしてください。初回デプロイのテストで使用できます。

もう一つの方法は、clasp (Google Apps Script CLI) を使ってデモプロジェクトを git clone し、その後コードをプッシュすることです。

コピーが完了すると、サンプルプロジェクトと全く同じになります。

  1. 初回デプロイ「ウェブアプリケーション」で結果を確認する

プロジェクト右上の「デプロイ」→「新しいデプロイを追加」→ タイプ「ウェブアプリケーション」:

実行権限:

  • 私は「すべてあなたのアカウントでスクリプトを実行します。」

  • ウェブアプリの利用者は、現在ログインしている 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.gsgithubToken 変数にトークンを入力してください。

GithubStub を GitHub に置き換える:

到 GAS プロジェクト → Settings.gs → 次のように設定:

const iOSGitHub = new GitHubStub(githubToken, iOSRepoPath);

変更して

const iOSGitHub = new GitHub(githubToken, iOSRepoPath);

ファイルを保存する。

テスト用「ウェブアプリケーション」URLをリロードして変更が正しいか確認する:

正しくデータが表示されているということは: GitHubが本物のデータに正常に接続できた 🎉🎉🎉

「Runner 状態」に切り替えて、Self-hosted Runner の状態が正常に取得できているか確認できます:

注:私のランナーは起動していないため、オフライン状態です。

Slack 連携

Slack通知を連携するために、まずリポジトリのGitHub Actionsに戻り、パッケージビルドActionのワークフロー通知用コンテナActionを新規作成します。

CD-Deploy-Form.yml:

# ワークフロー(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.gsslackBotToken 変数にトークンを入力します。

再び GAS プロジェクト → Settings.gs → 次のように変更してください:

const slack = new SlackStub(slackBotToken);

変更して

const slack = new Slack(slackBotToken);

ファイルを保存する。

既存の Slack Bot App がなく通知を送るのが面倒な場合は、ここでのすべての手順を無視し、GAS プロジェクト内の slack に関する使用部分を削除してください。

GitHub 連携 — パッケージフォーム

GAS プロジェクト → Controller_iOS.gsView_iOS_Form.html の内容を調整:
仮の Asana タスク連携方法を削除:

      <? tasks.forEach(function(task) { ?>
      <option value="<?=task.githubBranch?>">[<?=task.id?>] <?=task.title?></option>
      <? }) ?>

ここでデフォルトブランチ(ここでは main )を自分で調整することもできます。

GAS プロジェクト → Controller_iOS.gsiOSLoadForm() の内容を調整:

  • template.tasks = Stubable.fetchStubAsanaTasks(); の Asana のモック接続メソッドを削除してください。
    Asana/Jira と連携する場合は、直接 ChatGPT に連携方法を尋ねてください。

  • template.prs = iOSGitHub.fetchOpenPRs(); は実際に GitHub API を使ってオープン中の PR リストを取得します。必要に応じて残してください。

送信後の処理 iOSSubmitForm() の内容:

実際の GitHub Actions Workflow ファイル名や workflow_dispatch の inputs パラメータに応じて調整してください:

  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 RepoiOSGitHub.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に渡すことも可能です):

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認証プロセスが未完了

アクセス権「XXX」はGoogle認証手続きを完了していません。

— — —

プロジェクト連携

接続に戻ると、Firebase は ScriptApp.getOAuthToken() を使って実行ユーザーの権限に応じて動的にトークンを取得するため、トークンの設定は不要です。

GAS プロジェクト → Settings.gs → に移動して、以下を設定してください:

const iOSFirebase = new FirebaseStub(iOSFirebaseProject);

変更する

const iOSFirebase = new Firebase(iOSFirebaseProject);

すぐに可能です。

テスト用「ウェブアプリケーション」URLを打包記録にリロード → 記録の一つを選んで「ダウンロードリンクを取得」をクリック:

Firebase App Distribution のリリースノートで対応する Job Run Id のビルドが見つかった場合、ダウンロード情報が直接表示され、ダウンロードをクリックすると直接ダウンロードページに移動します。

完了!🎉🎉🎉

成果

Demo Web App

Demo Web App

ここまでで、サンプルはすべて実際に使えるパッケージングツールに変更済みです。残りのカスタマイズ機能や、さらに多くのサードパーティAPI連携、追加のフォームはご自身で拡張可能です(ChatGPTと相談しながら)。

最後に、開発とテストが完了したら必ず前述の手順に従い — デプロイの更新を行わないと反映されません!

連携拡張

「中継ステーション」役割の精神を受け継ぎ、ここにいくつかの迅速な連携用チートシートを提供します:

Asana API — タスクの取得:


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")

Jira API — チケット取得 (JQL):

// 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 を代わりに使用できます:

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

Slack API とメッセージ送信方法:

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

本シリーズの記事は多くの時間と労力をかけて執筆しました。内容があなたやチームの作業効率や製品品質の向上に役立った場合は、ぜひコーヒーをご馳走してください。ご支援ありがとうございます!

Buy me a coffee

🍺 Buy me a beer on PayPal

シリーズ記事:

*Post MediumからZMediumToMarkdownによって変換されました。

GitHub で編集
この記事を改善
本記事は Medium で初公開
オリジナルを読む
この記事をシェア
リンクをコピー · SNS でシェア
ZhgChgLi
著者

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

コメント