Post

CI/CD 打包工具平台|使用 Google Apps Script Web App 串接 GitHub Actions 提升跨团队效率

打造免费且易用的打包工具平台,解决非工程人员操作 GitHub Actions 的复杂性与权限控管问题,透过 Google Apps Script Web App 整合 GitHub、Slack、Firebase 等 API,实现跨团队任务单打包与即时进度通知,提升开发与验证效率。

CI/CD 打包工具平台|使用 Google Apps Script Web App 串接 GitHub Actions 提升跨团队效率

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


CI/CD 实战指南(四):使用 Google Apps Script Web App 串接 GitHub Actions 建置免费易用的打包工具平台

GAS Web App 串接 GitHub, Slack, Firebase 或 Asana/Jira API,建置中继站,提供跨团队共用的打包工具平台

Photo by [Lee Campbell](https://unsplash.com/@leecampbell?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash){:target="_blank"}

Photo by Lee Campbell

前言

前篇「 CI/CD 实战指南(三):使用 GitHub Actions 实作 App iOS CI 与 CD 工作流程 」我们已经完成了 App iOS 专案的 CI/CD 底层功能建设,现在已经可以 CI 自动测试验证跟 CD 打包部署;然而在实务产品开发流程上, 打包部署工作多半是为了后续交付给其他职能伙伴 进 行 QA(Quality Assurance) 功能验证,这时候 CD 场景就不在只局限在工程端了,可能跨足 QA、PM、设计(Design QA) 甚至是老板想先玩看看。

GitHub Actions workflow_dispatch 手动表单触发事件,虽然能提供简易的表单让使用者操作打包,但如果操作的对象是非工程人员就相对很不友善,他们不懂:什么是分支?栏位要不要填?怎么知道打包好了?好了怎么下载?…etc 另外还有权限控管问题,如果要让其他职能伙伴直接使用 GitHub Actions 打包,就需要把他的帐号加到 Repo 当中, 资安控管上很不安全也不合理 ,只是要操作打包表单却要开整个 Source Code 给他。

不同于 Jenkins 有独立的 Web 工具平台,GitHub Actions 就只有这个功能。

`workflow_dispatch 的表单样式`

workflow_dispatch 的表单样式

因此 我们需要一个中继站打包平台,用来服务其他职能使用者 ,整合 Asana/Jira 的任务单,让使用的人可以直接用任务单打包 App,并且直接在上面查看进度、下载打包结果。

GAS Web App 中继站

GAS Web App 中继站

上一篇 关注的是右边核心 GitHub Actions CI/CD 工作流程开发;本文关注的是左半部 to End-User 打包工具平台、增加使用者体验的部分。

Google Apps Script — Web App 打包工具平台 成果图

  • 打包表单: 整合专案管理工具捞取单号、整合 GitHub 捞取开启中的 Pull Request

  • 把包纪录: 显示打包历史纪录、正在打包的任务进度状态、点击取得下载连结捞取 Firebase App Distribution 下载连结、资讯

  • Runner 状态: 显示 Self-hosted Runner 状态。

  • Slack 打包进度通知。

  • 支援手机版

  • 支援限制组织团队内帐号使用

主要职责

0 状态、0 资料库, 单纯为中继交换站 ,整合显示各个 API 资料 (e.g. Asana/Jira/GitHub)、转发表单请求到 GitHub Actions。

操作要求: 支援手机与电脑。

权限要求: 能限制只有团队组织成员能存取。

Online Demo Web App

Sign in Edit description script.google.com

技术选择

第一篇文章 有提到过,这边再详细整理一次。

整合进 Slack

我们尝试过让 Slack 担任打包平台,自行开发后端服务并架设在 GCP 上,与 Slack API、Asana API 进行串接整合,再将 Slack 送来的表单转手发送给 GitHub API 触发后续 GitHub Actions;体验上非常爽而且统一在团队的协作工具上,可以无痛使用; 缺点是开发及后续维护成本很高 ,因为是自己用 Ktor 开发的后端服务,需要 App 工程师同时兼做后端、处理 Google 服务 OAuth 整合问题、部分功能可能在这做掉(例如:送审)…功能复杂;如果后来的新人没衔接好这整块几乎没办法维护下去了,另外每个月还要花 $15 USD GCP 伺服器费用。

初期也尝试过用 FaaS 服务串接 Slack API,例如 Cloud Funtions,但是 Slack API 要求串接的服务要能在 3 秒内回应请求,否则视为失败;FaaS 会有 冷启动问题 ,当服务一段时间没呼叫就会进入休眠,再次呼叫时会需要较长时间才能响应 (≥ 5秒),会造成 Slack 打包表单很不稳定,时常出现 Timeout Error。

整合进内部系统

这当然是最优解,如果团队有 Web、后端人力,直接与现有系统整合是最好也最安全的。

本文的前提是:没有,App 自立自强。

Google Apps Script — Web App

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 生态系相关服务(e.g. Firebase, GA…etc)与资料 (不用自己做 OAuth)

  • ✅ 程式语言使用 JavaScript 易上手 (V8 Runtime 支援 ES6+ )

  • ✅ 快速撰写、快速上线、快速使用

  • ✅ 服务稳定、长久 (已推出超过 16 年)

  • ✅ AI Can Help! 实测使用 ChatGPT 辅助开发,准确率可达 95%

Google Apps Script 缺点:

  • ❌ 内建的版本管控一言难尽

  • ❌ 内建不支援档案、资料储存、金钥/凭证管理

  • ❌ Web App 无法做到 100% 体验的 RWD

  • ❌ 专案只能绑在个人帐号而不是组织

  • ❌ 虽然 Google 有持续在开发维护,但整体功能更新缓慢

  • ❌ 网路请求 UrlFetchApp 不支援设定 User-Agent

  • ❌ Web App doGet / doPost 不支援取得 Headers 资讯

  • ❌ FaaS 冷启动问题

  • 不支援多人同时开发 但在 Web App 影响不大,顶多要多等几秒才进入网页。

以上是 GAS 本身服务的优缺点,对做打包工具 Web 影响不大;选择使用此方案对比 Slack 方案,更快速、轻量跟容易交接上手, 缺点是团队需要多知道这个工具的网址在哪跟怎么用、还有因 GAS 程式库功能有限 (e.g. 无内建加密演算法库) 基本上只能做纯中继平台,例如送审,那也只能转发送审请求请 GitHub Actions 做。

另外也仅适用是 Google Workspace 工作环境的团队; 权衡了资源与需求,因此采用 Google Apps Script — Web App 来实现打包工具平台。

UI Framework

我们直接使用 Bootstrap CDN,不然要自己弄 CSS 样式太麻烦了,Bootstrap 问 AI 怎么组合使用也更准确方便。

动手做

这边已经把整个平台架构开源了,大家可以依照自己团队的需求基于这个版本再客制化即可。

开源范例专案

在 GAS 上直接检视专案:

Google Drive: Sign-in Access Google Drive with a Google account (for personal use) or Google Workspace account (for business use) . script.google.com

GitHub Repo 备份:

档案架构

写了一个很阳春的 类-MVC 架构,如要调整或不知道功能问 AI 都能得到准确答案。

系统

  • appsscript.json: GAS 系统的 Metadata 设定档案 重点是「oauthScopes」变数,宣告这个 Script 会用到的外部权限。

  • Entrypoint.gs: 定义 doGet( ) 进入点

Controller

  • Controller_iOS.gs: iOS 打包工具页面 Controller,负责捞数据给 View 显示

View

  • View_index.html: 整个打包工具骨架、首页

  • View_iOS.html: iOS 打包工具页面骨架

  • View_iOS_Runs.html: iOS 打包工具 — 打包纪录内容页面

  • View_iOS_Form.html: iOS 打包工具 —打包表单页面

  • View_iOS_Runners.html: iOS 打包工具 — Self-hosted Runner 状态页面

Model(Lib)

  • Credentials.gs: 定义金钥内容 (⚠️️️请注意️,GAS 如果要走 GCP IAM 会蛮复杂的因此我们直接定义金钥在此, 所以这个 GAS 专案会包含机密资讯,请不要随意共享专案检视编辑权限 )

  • StubData.gs: Online Demo 用的 Stub 方法、资料。

  • Settings.gs: 一些常用设定,跟 lib init。

  • GitHub.gs: GitHub API 操作封装。

  • Slack.gs: Slack API 操作封装。

  • Firebase.gs: Firebase — App Distribution API 操作封装。

建立自己的打包平台

1.建立一个 Google Apps Script 专案 、命名

到专案设定 → 勾选「在编辑器中显示「appsscript.json」资讯清单档案」才会出现「appsscript.json」Metadata 档案。

2.参考我的 开源专案档案 ,把所有档案按照范例建立好跟把内容无脑复制过来

很蠢,但没办法。

StubData.gs 先一起复制过来,第一次部署测试可以使用。

另一个方法是用 clasp (Google Apps Script CLI) git clone demo project 后把 Code 推上去。

复制完长得会跟范例专案一模一样。

  1. 首次部署「网页应用程式」查看结果

专案右上角「部署」 → 「新增部署作业」 → 类型「网页应用程式」:

执行身份:

  • 统一都用你的帐号身分执行脚本。

  • 存取网页程式的使用者 会以当前登入的 Google 帐号使用者身份执行脚本。

谁可以存取:

  • 只有我自己

  • XXX 同个组织中的所有使用者 只有同组织+已登入的 Google 帐号使用者可以存取。

  • 所有已登入 Google 帐号的使用者 已登入的 Google 帐号使用者都可以存取。

  • 所有人 不需要登入 Google 帐号、所有人都可以公开存取。

如果是内部工具:可以选择「 谁可以存取:XXX 同个组织中的所有使用者」+「执行身份:存取网页程式的使用者」进行安全控管。

完成部署后的「网页应用程式」网址,就是你的 Web App 打包工具网址,可以分享给团队成员使用。(网址很丑,可以自己用短网址服务包装一下、 更新部署内容不会异动网址 )

使用者首次使用需要同意授权

首次点击 Web App 网址,需要先同意授权。

  • Review Permission → 选择要使用此 Web App 的帐户身份

  • 未验证警告视窗,点击「进阶」展开 → 点击 前往「XXX」(不安全)

  • 点击「允许」

尔后如果脚本权限没更动不用重新授权。

完成授权同意后就会进入打包工具首页:

Demo 打包工具部署成功 🎉🎉🎉

注:「这个应用程式是由 Google Apps Script 的使用者建立」这个提示无法自动隐欌。

更新部署

⚠️所有程式码的变动都需要更新部署才会生效。

️️⚠️所有程式码的变动都需要更新部署才会生效。

⚠️所有程式码的变动都需要更新部署才会生效。

这边要注意程式码变动储存完不会直接变动到 Web App 上,所以发现重整没效果就是这个原因; 需要到「部署」 → 「管理部署作业」→「编辑」→ 版本「建立新版本」 → 点击「部署」 → 「完成」

更新部署完再重整网页就能看到改动生效。

新增测试部署方便开发

如同前述,所有改动要生效都要更新部署;这在开发阶段非常麻烦,因此在开发阶段我们可以用「测试部署」来快速验证改动是否正确。

到「部署」 → 「测试部署作业」 → 取得测试用「网页应用程式」网址。

在开发阶段我们直接使用此网址就能储存,档案更改储存完,回到这个开发用网址重整网页就能看到成果!

都开发好之后再照前文说的更新部署,释出给使用者使用。

修改 Demo 范例专案串接真实资料

再来才是重点,串接真实资料,GitHub Actions Workflow 参考的是 上一篇文章中建立好的 CI/CD 流程 ,你也可依照实际的 Actions Workflow 调整参数。

⚠️修改前请注意

Google Apps Script 平台并不能很好的支援多人或甚至多开视窗开发,我自己踩过的雷是不小心开了两个编辑视窗,在 A 编辑完,后来在 B 编辑,所以修改都被 B 的旧版覆盖掉了;因此 建议同时间只有一个人一个视窗在编辑 Script

GitHub 串接

带入 GitHub API Token:

GitHub -> 帐号 -> Settings -> Developer Settings -> Fine-grained personal access tokens or Personal access tokens (classic)。

建议使用 Fine-grained personal access tokens 比较安全(但有期限)。

Fine-grained personal access tokens 需要的权限如下:

  • Repo: 记得选要操作的 Repo

  • Permissions: Actions (read/write)Administration (read only)

如果不想依赖在某人的帐号上建议建立一支干净的团队 GitHub 帐号,使用它的 Token。

到 GAS 专案 → Credentials.gs → 将 Token 带入到 githubToken 变数中。

替换 GithubStub 成 GitHub:

到 GAS 专案 → Settings.gs → 将:

1
const iOSGitHub = new GitHubStub(githubToken, iOSRepoPath);

改成

1
const iOSGitHub = new GitHub(githubToken, iOSRepoPath);

储存档案。

重新整理测试用「网页应用程式」网址查看改动是否正确:

能正确显示资料代表: GitHub 改串真实资料成功 🎉🎉🎉

也能顺手切到「Runner 状态」查看 Self-hosted Runner 状态捞取正不正常:

注:我 Runner 没开…所以是离线中。

Slack 串接

为了串接 Slack 通知,我们首先要回到 Repo → GitHub Actions 新增一个包裹打包 Action Workflow 的通知容器 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
# Workflow(Action) 名称
name: CD-Deploy-Form

# Actions Log 的标题名称
run-name: "[CD-Deploy-Form] ${{ github.ref }}"

# 同个 Concurrency Group 如果有新的 Job 会取消正在跑的
# 例如 重复触发相同分支的打包任务,会取消前一个任务
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# 触发事件
on:
  # 手动表单触发
  workflow_dispatch:
    # 表单 Inputs 栏位
    inputs:
      # App 版本号
      VERSION_NUMBER:
        description: 'Version Number of the app (e.g., 1.0.0). Auto-detect from the Xcode project if left blank.'
        required: false
        type: string
      # App Build Number
      BUILD_NUMBER:
        description: 'Build number of the app (e.g., 1). Will use a timestamp if left blank.'
        required: false
        type: string
      # App Release Note
      RELEASE_NOTE:
        description: 'Release notes of the deployment.'
        required: false
        type: string
      # 触发者的 Slack User ID
      SLACK_USER_ID:
        description: 'Slack user id.'
        required: true
        type: string
      # 触发者的 Email
      AUTHOR:
        description: 'Trigger author email.'
        required: true
        type: string
        
# Job 工作项目
jobs:
  # 开始打包时传送 Slack 讯息
  # Job ID
  start-message:
    # 小工作直接用 GitHub Hosted Runner 跑,用量不大
    runs-on: ubuntu-latest
    
    # 设定最长 Timeout 时间,防止异常情况发生时无止尽的等待
    # 正常情况不可能跑超过 5 分钟
    timeout-minutes: 5

    # 工作步骤
    steps:
      - name: Post a Start Slack Message
        id: slack
        uses: slackapi/slack-github-action@v2.0.0
        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 }}>"
    # Job Output 给后续 Job 使用
    # ts = Slack 讯息 ID,后续通知才可以 Reply 在同个 Threads
    outputs:
      ts: ${{ steps.slack.outputs.ts }}

  deploy:
    # Job 预设是并发执行,用 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: Post a Success Slack Message
        uses: slackapi/slack-github-action@v2.0.0
        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: Post a Failure Slack Message
        uses: slackapi/slack-github-action@v2.0.0
        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: Post a Cancelled Slack Message
        uses: slackapi/slack-github-action@v2.0.0
        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 Token 值

回到 GAS 专案 → Credentials.gs → 将 Token 带入到 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 串接方法:

      <? 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 拿 Opened PR List,可依需求保留。

送出后的处理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 参数即可。

重新整理测试用「网页应用程式」网址查看改动是否正确:

可以看到打包表单就只剩 Opened PR List 了。

填好资料按「送出请求」测试看看打包表单:

提示送出成功代表没问题,回到打包纪录也能看到任务开始执行了 🎉

重复点打包纪录能更新进度。

常见送出错误:

Required input ‘SLACK_USER_ID’ not provided : GitHub Actions 这个SLACK_USER_ID 栏位为必填,但没有带,可能是 Slack 设定失败、当前 User Email 找不到对应的 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 Acitons CD-Deploy.yml 打包作业时,把 Job Run ID 带入到 Release Note (当然也可以带到 Build Number):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ID="${{ github.run_id }}" // Job Run ID
COMMIT_SHA="${{ github.sha }}"
BRANCH_NAME="${{ github.ref_name }}"
AUTHOR="${{ env.AUTHOR }}"

# 组合 Release Note
RELEASE_NOTE="${{ env.RELEASE_NOTE }}
ID: ${ID}
Commit SHA: ${COMMIT_SHA}
Branch: ${BRANCH_NAME}
Author: ${AUTHOR}
"

# 执行 Fastlane 打包&部署 Lane
bundle exec fastlane beta release_notes:"${RELEASE_NOTE}" version_number:"${VERSION_NUMBER}" build_number:"${BUILD_NUMBER}"

这样 Firebase App Distribution Release Notes 就会有 Job Run ID。

GAS Web App 打包工具平台会串 GitHub API 取得 GitHub Actions 执行纪录我们直接用 API 给的 Job Run ID 带到 Firebase App Distribution API 查询 Release Notes 中有包含 *ID: XXX* 的版本,就能找到对应的打包纪录了。

不用任何资料库也能做到两个工具平台的对应。

串接专案设定

到 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() 依照执行身份动态取得 Token 使用,因此不需要设定 Token。

只需要到 GAS 专案 → Settings.gs → 将:

1
const iOSFirebase = new FirebaseStub(iOSFirebaseProject);

改成

1
const iOSFirebase = new Firebase(iOSFirebaseProject);

即可。

重新整理测试用「网页应用程式」网址到打包纪录 → 找一笔纪录点击「取得下载连结」:

如果有在 Firebase App Distribution Release Notes 找到对应的 Job Run Id 打包,就会直接显示下载资讯跟点下载能直接导到下载页。

完成!🎉🎉🎉

成果

至此你已经把范例都改成您的实际可用的打包工具,剩下的客制化功能、更多第三方 API 串接、更多表单可以自行延伸 (跟 ChatGPT 讨论)。

最后不要忘记都开发好测试完毕要上线,需要照前文步骤 — 更新部署作业才会生效喔!

串接延伸

延续我们「中继站」角色的精神,这边提供几个快速串接的 CheatSheet:

Asana API — 取得 Tasks:

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

Jira API — 取得 Tickets (JQL):

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("Failed to fetch Jira issues."); 
    }

    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 Sheet 代替:

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"); // Sheet Name
    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");
// Write
saveable.write("birthday_zhgchgli", "0718");
// Read
saveable.read("birthday_zhgchgli"); // -> 0718

Slack API & 发送讯息方法:

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(`Failed to send Slack message: ${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 工作流程,提升效率与产品稳定性;有任何实作问题欢迎留言讨论,这四篇文章大约花了 14+ 天撰写, 如果您觉得不错欢迎 Follow 我的 Medium 和与朋友同事分享

谢谢。

Buy me a coffee

本系列文章花费了大量的时间精力撰写,如果内容对您有帮助、对您的团队有实质提升工作效率与产品品质;欢迎请我喝杯咖啡,感谢支持!

[Buy me a coffee](https://www.buymeacoffee.com/zhgchgli){:target="_blank"}

Buy me a coffee

系列文章:

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.