Post

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

Integrating GAS Web App with GitHub, Slack, Firebase, or Asana/Jira API to create 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 with Google Apps Script Web App and 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 with Google Apps Script Web App and GitHub Actions

Integrating GAS Web App with GitHub, Slack, Firebase, or Asana/Jira API to create 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 with GitHub Actions,” we completed the foundational features 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 often meant 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 take a look first.

The GitHub Actions workflow_dispatch manual form trigger event provides 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.

Unlike Jenkins, which has an independent web tool platform, GitHub Actions only has this functionality.

`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 list and view progress and download packaging 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, current 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) and 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’s 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 then forwarding the forms sent from Slack to GitHub API to trigger subsequent GitHub Actions. 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, manage Google service OAuth integration issues, and some functions might be complex (e.g., submission for review). If new team members do not properly transition this part, it becomes nearly impossible to maintain. Additionally, there is a monthly cost of $15 USD for GCP server fees.

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 within 3 seconds; otherwise, it is considered a failure. FaaS can have cold start issues, where if the service has not been called for a while, it goes into sleep mode, and the next call takes longer to respond (≥ 5 seconds), leading to instability in the Slack packaging form and frequent Timeout Errors.

Integration with Internal Systems

This is certainly 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 stands on its own.

Google Apps Script — Web App

Google Apps Script is our old friend; we have previously completed many RPA projects using it 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 remembered 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 hit the limits
  • ✅ Functions as a Service, no need to set up and maintain servers.
  • ✅ Permission control similar to Google Workspace, allowing access only to Google accounts within the organization.
  • ✅ Seamless integration with Google ecosystem services (e.g., Firebase, GA, etc.) and data (no need to handle OAuth yourself).
  • ✅ Programming language is JavaScript, 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 support for file, data storage, and key/certificate management is absent.
  • ❌ Web App cannot achieve 100% responsive web design.
  • ❌ 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 retrieving header information.
  • ❌ FaaS cold start issues.
  • Does not support simultaneous development by multiple users. 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 creating the packaging tool web. Choosing this solution compared to the Slack option is faster, lighter, and easier to hand over. The downside is that the team needs to be aware of where the tool’s URL is and how to use it, and due to the limited functionality of GAS libraries (e.g., no built-in encryption algorithm library), it can essentially 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 use Bootstrap CDN; otherwise, creating CSS styles would be too troublesome. Asking AI how to combine and use Bootstrap is also more accurate and convenient.

Getting Started

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 pseudo-MVC structure. If you need to adjust or are unsure about functionality, you can ask AI for accurate answers.

System

  • appsscript.json: GAS system metadata configuration file. 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 skeleton of the entire packaging tool, homepage.
  • View_iOS.html: The skeleton of the iOS packaging tool page.
  • 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 is to use GCP IAM, it can be quite complex, so we directly define the keys here. This GAS project will contain confidential information, so 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 make the “appsscript.json” metadata file appear.

  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 choice; 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 run the script.
  • Users accessing the web app will run the script as the currently logged-in Google account user.

Who can access:

  • Only myself.
  • XXX All users in the same organization only users from the same organization + logged-in Google account users 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 will be 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.)

Web App Authorization and Deployment Guide

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

  • Review Permission → Choose the account you want to use for this Web App
  • Unverified warning window, click “Advanced” to expand → Click “Go to ‘XXX’ (unsafe)”

  • Click “Allow”

After this, if the script permissions remain unchanged, you do not need to reauthorize.

Once authorization is complete, you will enter the homepage of the packaging tool:

Demo packaging tool deployed successfully 🎉🎉🎉

Note: The message “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”.

After updating the deployment, refreshing the webpage will show the changes taking effect.

Add Test Deployment for Easier Development

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

Go to “Deploy” → “Test Deployment” → Get the test “Web Application” URL.

During the development phase, we can directly use this URL to save changes. 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, and you can adjust 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 have personally encountered issues when I accidentally opened two editing windows; after editing in A, I later edited in B, causing the changes 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:

  • 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 else’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:

1
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 displays correctly, it means: GitHub has successfully connected to real data 🎉🎉🎉

You can also easily switch to “Runner Status” to check if the Self-hosted Runner 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

# Actions Log title
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 tasks run directly on GitHub Hosted Runner, not resource-intensive
    runs-on: ubuntu-latest
    
    # Set maximum timeout duration to prevent endless waiting in case of abnormal situations
    # 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 that connects to Slack notifications; it actually reuses the CD-Deploy.yml Action written in the previous article.

  • For creating a 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 connection 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 connection to Asana. If you want to connect to Asana/Jira, you can directly ask ChatGPT to help you create the connection method.
  • template.prs = iOSGitHub.fetchOpenPRs(); is the actual connection to the GitHub API to get the Opened PR List, which you can keep as needed.

Processing After Submission in 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 in, otherwise, it will throw an error message.

If you think this is not secure enough, you can add password verification or restrict usage to specific 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 connect to 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.

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 on the packaging records can update the progress.

Common Submission Errors:

Required input ‘SLACK_USER_ID’ not provided : The SLACK_USER_ID field in GitHub Actions is required, but it was not provided. This may be due to a failed Slack setup or the current User Email not matching a 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: This 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/install directly from the packaging platform tool on mobile.

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 GitHub Actions execution records, 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.

You can achieve the correspondence between two tool platforms without any database.

Project Setup for Integration

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

Enter the Firebase project number you want to integrate.

The initial setup may show 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 on the “OAuth consent screen details” link → Click “Configure consent screen”:

Click “Get Started”:

Application Information:

  • Application Name: Enter your tool name
  • User Support Email: Select email

Target Audience:

  • Internal: Only for use by partners within the organization
  • External: All Google account users can use it after consenting to authorization

Contact Information:

  • Enter an email for receiving notifications

Check the consent for the Google API Services: User Data Policy .

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”.

If no errors appear, the binding is complete.

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

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

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

Users can complete the authorization following the previous step of “User consent required for first-time use” and start using it!

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

Access blocked “XXX” not completed Google verification process

Access blocked “XXX” not completed 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.

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

1
const iOSFirebase = new FirebaseStub(iOSFirebaseProject);

To

1
const iOSFirebase = new Firebase(iOSFirebaseProject);

And 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 by yourself (discuss with ChatGPT).

Finally, don’t forget that after all development and testing are completed, you need to go online, following the previous steps — update the deployment operation for it to take effect!

Integration Extensions

Continuing with 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 series of articles on CI/CD from 0 to 1 comes to a close; I hope it can genuinely help you and your team build a robust CI/CD workflow, enhancing efficiency and product stability. If you have any implementation questions, feel free to leave a comment. 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.

Buy me a coffee

This series of articles took a significant amount of time and effort to write. If the content has helped you or your team improve work efficiency and product quality, feel free to buy me a coffee as a token of appreciation!

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

Buy me a coffee

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

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