Post

CI/CD Practical Guide (Part 4): Building a Free and User-Friendly Packaging Tool Platform Using Google Apps Script Web App with GitHub Actions

Integrating GAS Web App with GitHub, Slack, Firebase, or Asana/Jira API to build a relay station that provides a packaging tool platform for cross-team sharing.

CI/CD Practical Guide (Part 4): Building a Free and User-Friendly Packaging Tool Platform Using Google Apps Script Web App with GitHub Actions

ℹ️ℹ️ℹ️ The following content is translated by OpenAI.

Click here to view the original Chinese version. | 點此查看本文中文版


CI/CD Practical Guide (Part 4): Building a Free and User-Friendly Packaging Tool Platform Using Google Apps Script Web App with GitHub Actions

Integrating GAS Web App with GitHub, Slack, Firebase, or Asana/Jira API to build a relay station that provides a packaging tool platform for cross-team sharing.

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

Photo by Lee Campbell

Introduction

In the previous article, “CI/CD Practical Guide (Part 3): Implementing iOS CI and CD Workflows Using GitHub Actions,” we completed the foundational functionality for the CI/CD of the iOS app project. We can now perform CI automated testing and CD packaging deployment. However, in practical product development processes, packaging and deployment tasks are mostly intended for subsequent delivery to other functional partners for QA (Quality Assurance) validation. At this point, the CD scenario is no longer limited to the engineering side; it may involve QA, PM, design (Design QA), or even the boss wanting to try it out first.

The GitHub Actions workflow_dispatch manual form trigger event can provide a simple form for users to operate packaging, but if the target users are non-engineers, it can be quite unfriendly. They may not understand: What is a branch? Do I need to fill in the fields? How do I know when the packaging is done? How do I download it? …etc. Additionally, there are permission control issues. If we want other functional partners to use GitHub Actions for packaging directly, we need to add their accounts to the repo, which is not secure or reasonable from a cybersecurity perspective; just to operate the packaging form, we have to expose the entire source code to them.

`workflow_dispatch Form Style`

workflow_dispatch Form Style

Therefore, we need a relay station packaging platform to serve other functional users, integrating Asana/Jira task lists so that users can package the app directly using the task lists and view progress and download results right there.

GAS Web App Relay Station

GAS Web App Relay Station

The previous article focused on the core GitHub Actions CI/CD workflow development on the right side; this article focuses on the left side, the end-user packaging tool platform, enhancing user experience.

Google Apps Script — Web App Packaging Tool Platform Results

  • Packaging Form: Integrates project management tools to fetch task numbers and integrates GitHub to fetch open Pull Requests.
  • Packaging Records: Displays packaging history, task progress status, and provides download links for Firebase App Distribution.
  • Runner Status: Displays the status of the self-hosted runner.
  • Slack Packaging Progress Notifications.
  • Mobile version support.
  • Supports restricting usage to accounts within the organization.

Main Responsibilities

0 status, 0 database, purely a relay exchange station, integrating and displaying data from various APIs (e.g., Asana/Jira/GitHub), forwarding form requests to GitHub Actions.

Operational Requirements: Supports both mobile and desktop.

Permission Requirements: Can restrict access to only team organization members.

Online Demo Web App

Sign in Edit description script.google.com

Technology Choices

The first article mentioned this, and here is a detailed summary again.

Integration with Slack

We attempted to use Slack as the packaging platform, developing our backend service and hosting it on GCP, integrating with Slack API and Asana API, and forwarding the forms sent from Slack to trigger subsequent GitHub Actions via GitHub API. The experience was very smooth and unified within the team’s collaboration tools, allowing for seamless usage; the downside is that the development and subsequent maintenance costs are very high, as we developed the backend service using Ktor, requiring app engineers to also handle backend tasks, Google service OAuth integration issues, and some functionalities might be complex (e.g., submission for review). If new team members do not transition well into this area, it becomes nearly impossible to maintain. Additionally, we incur a monthly cost of $15 USD for the GCP server.

Initially, we also tried using FaaS services to integrate with Slack API, such as Cloud Functions, but the Slack API requires the integrated service to respond to requests within 3 seconds; otherwise, it is considered a failure. FaaS has a cold start issue, where if the service is not called for a period, it goes into sleep mode, and subsequent calls take longer to respond (≥ 5 seconds), causing instability in the Slack packaging form and frequent Timeout Errors.

Integration with Internal Systems

This is, of course, the optimal solution. If the team has web and backend personnel, directly integrating with existing systems is the best and safest approach.

The premise of this article is: No, the app is self-sufficient.

Google Apps Script — Web App

Google Apps Script is our old partner; we have previously used it for many RPA projects to schedule and trigger tasks, such as “Crashlytics + Google Analytics Automatic Query for App Crash-Free Users Rate” and “Using Google Apps Script to Implement Daily Data Report RPA Automation.” This time, I thought of it again; GAS has a feature that allows deployment as a Web (App) to serve as a web service.

Advantages of Google Apps Script:

  • ✅ Free, under normal usage, you hardly touch the limits
  • ✅ Functions as a Service, no need to set up and maintain servers.
  • ✅ Permission control is the same as Google Workspace, allowing settings for use only by Google accounts within the organization.
  • ✅ Seamless integration with Google ecosystem services (e.g., Firebase, GA, etc.) and data (no need to handle OAuth).
  • ✅ Programming language is JavaScript, which is easy to learn (V8 Runtime supports ES6+).
  • ✅ Quick to write, deploy, and use.
  • ✅ Stable and long-lasting service (has been available for over 16 years).
  • ✅ AI Can Help! Tested using ChatGPT for development assistance, achieving an accuracy rate of up to 95%.

Disadvantages of Google Apps Script:

  • ❌ Built-in version control is lacking.
  • ❌ Built-in does not support file, data storage, or key/certificate management.
  • ❌ Web App cannot achieve 100% responsive web design experience.
  • ❌ Projects can only be tied to personal accounts, not organizations.
  • ❌ Although Google continues to develop and maintain it, overall feature updates are slow.
  • ❌ Network requests UrlFetchApp do not support setting User-Agent.
  • ❌ Web App doGet / doPost do not support obtaining header information.
  • ❌ FaaS cold start issue.
  • Does not support simultaneous development by multiple people. However, this has little impact on the Web App; at most, you may have to wait a few extra seconds to enter the webpage.

The above are the pros and cons of GAS itself, which have little impact on building the packaging tool web. Choosing this solution compared to the Slack solution is faster, lighter, and easier to hand over. The downside is that the team needs to know where the tool’s URL is and how to use it, and due to the limited functionality of the GAS library (e.g., no built-in encryption algorithm library), it can only serve as a pure relay platform, such as for submission, which can only forward submission requests to GitHub Actions.

Additionally, it is only suitable for teams in a Google Workspace environment; balancing resources and needs, we chose Google Apps Script — Web App to implement the packaging tool platform.

UI Framework

We directly used Bootstrap CDN; otherwise, setting up CSS styles would be too troublesome. Asking AI how to combine and use Bootstrap is also more accurate and convenient.

Hands-On

The entire platform architecture has been open-sourced, and everyone can customize it based on their team’s needs from this version.

Open Source Example Project

View the project directly on 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 Backup:

File Structure

I wrote a very basic class-MVC structure; if you need to adjust or are unsure about functionalities, you can ask AI for accurate answers.

System

  • appsscript.json: Metadata configuration file for the GAS system. The key point is the “oauthScopes” variable, which declares the external permissions this script will use.
  • Entrypoint.gs: Defines the entry point for doGet().

Controller

  • Controller_iOS.gs: iOS packaging tool page controller, responsible for fetching data for the view to display.

View

  • View_index.html: The entire packaging tool skeleton, homepage.
  • View_iOS.html: iOS packaging tool page skeleton.
  • View_iOS_Runs.html: iOS packaging tool — packaging record content page.
  • View_iOS_Form.html: iOS packaging tool — packaging form page.
  • View_iOS_Runners.html: iOS packaging tool — self-hosted runner status page.

Model (Lib)

  • Credentials.gs: Defines key content. (⚠️️️ Please note, if GAS needs to go through GCP IAM, it can be quite complex, so we directly define the keys here. Therefore, this GAS project will contain confidential information; please do not share project view/edit permissions casually.)
  • StubData.gs: Stub methods and data for the online demo.
  • Settings.gs: Some common settings and lib initialization.
  • GitHub.gs: GitHub API operation encapsulation.
  • Slack.gs: Slack API operation encapsulation.
  • Firebase.gs: Firebase — App Distribution API operation encapsulation.

Building Your Own Packaging Platform

  1. Create a Google Apps Script project and name it.

Go to project settings → check “Show ‘appsscript.json’ manifest file in editor” to display the “appsscript.json” metadata file.

  1. Refer to my open-source project files, create all files according to the example, and copy the content over.

It’s silly, but there’s no other way; GAS does not support Git version control.

Copy StubData.gs over first; it can be used for the initial deployment test.

After copying, it will look exactly like the example project.

  1. Deploy the “Web App” for the first time to check the results.

In the upper right corner of the project, click “Deploy” → “New deployment” → Type “Web App”:

Execution Identity:

  • I always use your account identity to execute the script.
  • Users accessing the web app will execute the script as the currently logged-in Google account user.

Who can access:

  • Only myself.
  • XXX All users in the same organization only users within the organization and logged-in Google accounts can access.
  • All logged-in Google account users all logged-in Google account users can access.
  • Everyone no need to log in to a Google account; everyone can access publicly.

If it’s an internal tool: you can choose “ Who can access: XXX All users in the same organization” + “Execution identity: Users accessing the web app” for security control.

After completing the deployment, the “Web App” URL is your web app packaging tool URL, which you can share with team members for use. (The URL is quite ugly; you can use a URL shortening service to wrap it up, updating the deployment content will not change the URL.)

Users Need to Agree to Authorization on First Use

The first time you click the Web App URL, you need to agree to the authorization.

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
![](/assets/4273e57e7148/1*lckRkGtc7EVqz9aTZaBIOw.png)

![](/assets/4273e57e7148/1*l1t290T_4xs1ly-RjjZXtg.png)

![](/assets/4273e57e7148/1*O9ItCSBNoBcuqepd-6ygCA.png)

- Review Permission → Choose the account identity to use this Web App
- Unverified warning window, click "Advanced" to expand → Click Go to "XXX" \(unsafe\)

![](/assets/4273e57e7148/1*cYloLsBROLbZDPIciYUm5g.png)

- Click "Allow"

> _After this, if the script permissions do not change, you do not need to reauthorize._

**After completing the authorization consent, you will enter the homepage of the packaging tool:**

![](/assets/4273e57e7148/1*vG5SsVFHPPDukc4ej0hqLA.png)

> Demo packaging tool deployed successfully 🎉🎉🎉

Note: "This application was created by a user of Google Apps Script" cannot be automatically hidden.

#### Update Deployment

> ⚠️ All code changes require an update deployment to take effect.

> ⚠️ All code changes require an update deployment to take effect.

> ⚠️ All code changes require an update deployment to take effect.

Please note that code changes saved will not directly affect the Web App, so if you find that refreshing has no effect, this is the reason; **You need to go to "Deploy" → "Manage Deployments" → "Edit" → Version "Create New Version" → Click "Deploy" → "Done".**

![](/assets/4273e57e7148/1*VsfCEfwnPlx9RbQ8DtXpnA.png)

![](/assets/4273e57e7148/1*n62MVd6o8W3hUtQpfn9Q0w.png)

> _After updating the deployment, refreshing the webpage will show the changes taking effect._

#### Add Test Deployment for Development Convenience

![](/assets/4273e57e7148/1*4fLoW6jf8z1AIXvULW-AOA.png)

![](/assets/4273e57e7148/1*yvTxeaImQ4Mr7FwESsuf5A.png)

As mentioned earlier, all changes need to be updated in the deployment to take effect; this can be very cumbersome during the development phase, so we can use "Test Deployment" to quickly verify if the changes are correct.

**Go to "Deploy" → "Test Deployment" → Get the test "Web Application" URL.**

![](/assets/4273e57e7148/1*Rt0XEw9uAev58isLEnsoPA.png)

During the development phase, we can directly use this URL to save, and after saving the file changes, return to this development URL and refresh the webpage to see the results!

> **_Once development is complete, update the deployment as mentioned earlier to release it for user use._**

### Modify Demo Example Project to Connect Real Data

Next is the key point, connecting real data. The GitHub Actions Workflow refers to the [**CI/CD process established in the previous article**](../4b001d2e8440/), and you can adjust the parameters according to your actual Actions Workflow.

#### ⚠️ Please Note Before Modifying

The Google Apps Script platform does not support multi-user or even multiple window development very well. I personally encountered issues when I accidentally opened two editing windows; after editing in A, I later edited in B, causing the modifications to be overwritten by the old version in B. Therefore, **it is recommended that only one person edits the Script in one window at a time.**

#### GitHub Integration

**Input GitHub API Token:**

GitHub → Account → Settings → Developer Settings → Fine-grained personal access tokens or Personal access tokens \(classic\).

It is recommended to use Fine-grained personal access tokens for better security \(but they have an expiration\).

**The permissions required for Fine-grained personal access tokens are as follows:**

![](/assets/4273e57e7148/1*lCNQHwC4EMU4gNXYC-zs2A.png)

- Repo: Remember to select the Repo you want to operate on
- Permissions: `Actions (read/write)`, `Administration (read only)`

> _If you do not want to rely on someone's account, it is recommended to create a clean team GitHub account and use its Token._

Go to the GAS project → `Credentials.gs` → Input the Token into the `githubToken` variable.

**Replace GithubStub with GitHub:**

Go to the GAS project → `Settings.gs` → Change:
```javascript
const iOSGitHub = new GitHubStub(githubToken, iOSRepoPath);

To

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

Save the file.

Refresh the test “Web Application” URL to check if the changes are correct:

If the data is displayed correctly, it means: Successfully connected GitHub to real data 🎉🎉🎉

You can also easily switch to “Runner Status” to check if the Self-hosted Runner status is functioning properly:

Note: My Runner is not running… so it is offline.

Slack Integration

To integrate Slack notifications, we first need to return to the Repo → GitHub Actions and add a notification container Action for the packaging Action Workflow.

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
name: CD-Deploy-Form

# Title name for Actions Log
run-name: "[CD-Deploy-Form] ${{ github.ref }}"

# If there is a new Job in the same Concurrency Group, it will cancel the running one
# For example, if the same branch's packaging task is triggered repeatedly, it will cancel the previous task
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# Trigger events
on:
  # Manual form trigger
  workflow_dispatch:
    # Form Inputs fields
    inputs:
      # App version number
      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
      # Triggerer's Slack User ID
      SLACK_USER_ID:
        description: 'Slack user id.'
        required: true
        type: string
      # Triggerer's Email
      AUTHOR:
        description: 'Trigger author email.'
        required: true
        type: string
        
# Job tasks
jobs:
  # Send Slack message when packaging starts
  # Job ID
  start-message:
    # Small jobs can run directly on GitHub Hosted Runner, not resource-intensive
    runs-on: ubuntu-latest
    
    # Set the maximum Timeout duration to prevent endless waiting in case of anomalies
    # Normally, it should not take more than 5 minutes
    timeout-minutes: 5

    # Job steps
    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: "Received packaging request.\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 for subsequent Jobs to use
    # ts = Slack message ID, so subsequent notifications can Reply in the same Threads
    outputs:
      ts: ${{ steps.slack.outputs.ts }}

  deploy:
    # Jobs are executed concurrently by default, use needs to wait for start-message to complete before executing
    # Execute packaging deployment task
    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 }}

  # Success message for packaging deployment task
  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: "✅ Packaging deployment successful.\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"

  # Failure message for packaging deployment task
  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: "❌ Packaging deployment failed, please check the execution status or try again later.\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"

  # Cancelled message for packaging deployment task
  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: Packaging deployment has been cancelled.\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"

Complete Code: CD-Deploy-Form.yml

This Action is just a container for integrating Slack notifications, actually reusing the CD-Deploy.yml Action written in the previous article.

  • For creating Slack Bot App and setting message permissions, you can refer to my previous article.
  • Remember to go to Repo → Secrets to add the corresponding SLACK_BOT_TOKEN and input the Slack Bot App Token value.

Return to the GAS project → Credentials.gs → Input the Token into the slackBotToken variable.

Then go to the GAS project → Settings.gs → Change:

1
const slack = new SlackStub(slackBotToken);

To

1
const slack = new Slack(slackBotToken);

Save the file.

If you do not have an existing Slack Bot App to send notifications and do not want to create one, you can skip all these steps and remove any Slack-related usage in the GAS project.

GitHub Integration — Packaging Form

Go to the GAS project → Controller_iOS.gs → Adjust the content of View_iOS_Form.html: Remove the fake Asana Tasks integration method:

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

You can also adjust the default branch here (this is main).

Go to the GAS project → Controller_iOS.gs → Adjust the content of iOSLoadForm():

  • Remove the line template.tasks = Stubable.fetchStubAsanaTasks(); which is a fake integration method for Asana. If you want to integrate Asana/Jira, you can directly ask ChatGPT to help generate the integration method.
  • template.prs = iOSGitHub.fetchOpenPRs(); is the actual integration with the GitHub API to get the Opened PR List, which can be retained as needed.

Processing after submission in the iOSSubmitForm() content:

You can adjust according to the actual GitHub Actions Workflow file name and workflow_dispatch input parameters:

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)
  });

You can also add your own required validation; here it only validates that the branch must be filled, otherwise, an error message will be shown.

If you think this is not secure enough, you can add password validation or restrict usage to special accounts.

The last line Slack notification functionality requires proper Slack setup. If you do not have a Slack Bot App or do not want to integrate Slack, you can directly use iOSGitHub.dispatchWorkflow("CD-Deploy.yml") in the Demo Actions Repo and remove the SLACK_USER_ID parameter.

Refresh the test “Web Application” URL to check if the changes are correct:

You can see that the packaging form now only has the Opened PR List left.

Fill in the data and click “Submit Request” to test the packaging form:

A successful submission prompt indicates no issues, and you can also see the task starting to execute in the packaging records 🎉:

Repeatedly clicking the packaging records can update the progress.

Common submission errors:

Required input ‘SLACK_USER_ID’ not provided : This SLACK_USER_ID field is required in GitHub Actions, but it was not provided, which may be due to a failed Slack setup or the current User Email not finding the corresponding Slack UID.

Workflow does not have ‘workflow_dispatch’ trigger, Branch is outdated, please update xxx branch: The selected branch cannot find the corresponding Action Workflow file (the file specified in iOSGitHub.dispatchWorkflow).

No ref found for, Branch not found: The specified branch cannot be found.

The last small feature is to integrate Firebase App Distribution to directly obtain download information and links, making it convenient to click and download the installation directly on mobile devices.

Previously introduced in “ Google Apps Script x Google APIs Quick Integration Method “, GAS can quickly and painlessly integrate with Firebase.

Integration Principle

Before integration, let me explain this “Tricky” integration principle.

Our packaging platform does not have a database; it purely acts as an API relay station. Therefore, during the packaging operation in GitHub Actions CD-Deploy.yml, we pass the Job Run ID into the Release Note (it can also be passed into the 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 }}"

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

# Execute Fastlane packaging & deployment Lane
bundle exec fastlane beta release_notes:"${RELEASE_NOTE}" version_number:"${VERSION_NUMBER}" build_number:"${BUILD_NUMBER}"

This way, the Firebase App Distribution Release Notes will contain the Job Run ID.

The GAS Web App packaging tool platform will connect to the GitHub API to obtain the execution records of GitHub Actions, and we can directly use the API-provided Job Run ID to query the Firebase App Distribution API for versions that include *ID: XXX* in the Release Notes, allowing us to find the corresponding packaging records.

No database is needed to achieve the correspondence between the two tool platforms.

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
```markdown
![](/assets/4273e57e7148/1*AJRMWv_rqu64H0ZrY4q6QA.png)

![](/assets/4273e57e7148/1*Sdzt1MDXh7TNnMDkpc5uJg.png)

#### Project Setup for Integration

Go to GAS → Project Settings → Google Cloud Platform \(GCP\) Project → Change Project:

![](/assets/4273e57e7148/1*klclBbiBQXNBzbzzj1jH0Q.png)

![](/assets/4273e57e7148/1*gv8m_v5O_yyUrq8uQoverQ.png)

Enter the Firebase project number you want to integrate.

![](/assets/4273e57e7148/1*OEaxA6SZWYbiVstOK60hQg.png)

> **_The initial setup may trigger an error_** _“To change the project, please set up the OAuth consent screen. Details for setting up the OAuth consent screen.” If you don't have this, you can skip the following steps._

**Click the “OAuth consent screen details” link → Click “Configure consent screen”:**

![](/assets/4273e57e7148/1*oiZeO5mEqH-D3tHrJsnE1A.png)

**Click “Get Started”:**

![](/assets/4273e57e7148/1*NK9RG0kaXoxJB5X-B8vhww.png)

**Application Information:**
- Application Name: `Enter your tool name`
- User Support Email: `Select email`

**Target Audience:**
- Internal: For use only by partners within the organization
- External: All Google account users can consent and use after authorization

**Contact Information:**
- Enter an email for receiving notifications

**Check the consent for the [Google API Services: User Data Policy](https://developers.google.com/terms/api-services-user-data-policy?hl=zh_TW){:target="_blank"} .**

Finally, click “**Create**”.

—

Return to GAS → Project Settings → Google Cloud Platform \(GCP\) Project → Change Project:

Re-enter the Firebase project number and click “Change Project”.

![](/assets/4273e57e7148/1*g-Lo04WjhaDjtXeXthNtDg.png)

**If no errors appear, the binding is complete.**

—

**If you selected “External,” you may need to complete the following settings:**

Click “Project Number” → Expand the left sidebar → “API & Services” → “OAuth consent screen”

![](/assets/4273e57e7148/1*RJNQv8v3KZoTcVwUbsXSHQ.png)

Select “Target Audience” → Test Click “Publish App” → Complete.

![](/assets/4273e57e7148/1*8vqrxhchsILPy4eSdvXfNg.png)

> _Users can complete the authorization following the previous step “**Users need to consent for first-time use**” and start using it!_

**If the above steps are not set, users will encounter the following error:**

![Access blocked "XXX" did not complete the Google verification process](/assets/4273e57e7148/1*2omUPFoubsrXVLHBPFkPbg.png)

Access blocked "XXX" did not complete the Google verification process

— — —
#### Integration Project

Returning to the integration, Firebase directly uses `ScriptApp.getOAuthToken()` to dynamically obtain the Token based on the execution identity, so there is no need to set the Token.

You only need to go to the GAS project → `Settings.gs` → change:
```javascript
const iOSFirebase = new FirebaseStub(iOSFirebaseProject);

to

1
const iOSFirebase = new Firebase(iOSFirebaseProject);

That’s it.

Refresh the test “Web Application” URL to the build record → Find a record and click “Get Download Link”:

If you find the corresponding Job Run Id in the Firebase App Distribution Release Notes, it will directly display the download information and clicking download will take you to the download page.

Done! 🎉🎉🎉

Results

At this point, you have modified the examples into your actual usable packaging tool. The remaining custom features, more third-party API integrations, and additional forms can be extended on your own (discuss with ChatGPT).

Finally, don’t forget that once development and testing are complete, you need to follow the previous steps — update the deployment for it to take effect!

Integration Extensions

Continuing with the spirit of our “relay station” role, here are a few quick integration CheatSheets:

Asana API — Get Tasks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 — Get 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 = filter criteria
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)`);

If you really need a database, you can use Google Sheets instead:

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 & Sending Messages:

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;
  }
}

More Google Apps Script Examples:

Summary

Thank you for your patience in reading and participating. This concludes the CI/CD series from 0 to 1; I hope it can genuinely assist you and your team in building a robust CI/CD workflow, enhancing efficiency and product stability. If you have any implementation questions, feel free to leave a comment for discussion. These four articles took about 14+ days to write, if you found it helpful, please follow my Medium and share it with friends and colleagues.

Thank you.

Series Articles:

If you have any questions or suggestions, feel free to contact me.

```


This article was first published on Medium ➡️ Click Here

Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.

Improve this page on Github.

Buy me a beer

33 Total Views
Last Statistics Date: 2025-07-11 | 13 Views on Medium.
This post is licensed under CC BY 4.0 by the author.