Post

CI/CD Guide|Integrate Google Apps Script Web App with GitHub Actions for Free Packaging Platform

Developers facing complex CI/CD workflows can leverage Google Apps Script Web App integrated with GitHub Actions to build a free, user-friendly packaging tool platform that streamlines team collaboration and automates build processes efficiently.

CI/CD Guide|Integrate Google Apps Script Web App with GitHub Actions for Free Packaging Platform

点击这里查看本文章简体中文版本。

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

This post was translated with AI assistance — let me know if anything sounds off!


CI/CD Practical Guide (Part 4): Using Google Apps Script Web App with GitHub Actions to Build a Free and Easy Packaging Tool Platform

GAS Web App Integration with GitHub, Slack, Firebase, or Asana/Jira API to Build a Middleware Station, Providing a Cross-Team Shared Packaging Tool Platform

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

Photo by Lee Campbell

Preface

Previous article “CI/CD Practical Guide (3): Implementing App iOS CI and CD Workflow with GitHub Actions” covered the foundational CI/CD setup for the App iOS project. Now, CI automated testing and CD packaging deployment are functional. However, in real product development, packaging and deployment are mostly for delivering to other teams for QA (Quality Assurance) testing. At this point, the CD scenario extends beyond engineering, involving QA, PM, design (Design QA), and even the boss who wants to try it out first.

GitHub Actions workflow_dispatch manual form trigger event provides a simple form for users to operate packaging. However, it is not user-friendly for non-engineers. They don’t understand: what is a branch? Should the fields be filled? How do they know the package is ready? How to download it once done? …etc.
Additionally, there is a permission control issue. To allow other functional team members to use GitHub Actions for packaging directly, their accounts must be added to the repo. This is insecure and unreasonable for security control, as they get access to the entire source code just to operate the packaging form.

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

`Form style of workflow_dispatch`

Form Style of workflow_dispatch

Therefore, we need an intermediary packaging platform to serve other functional users, integrating Asana/Jira task tickets, allowing users to package apps directly from the task tickets and view progress and download packaging results on the platform.

GAS Web App Relay Station

GAS Web App Relay Station

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

Google Apps Script — Web App Packaging Tool Platform Results Screenshot

  • Package Forms: Integrate project management tools to fetch ticket numbers and connect GitHub to retrieve open Pull Requests

  • Package Records: Display packaging history, current packaging task progress, and allow clicking to get download links by fetching Firebase App Distribution download links and details.

  • Runner Status: Show the status of the Self-hosted Runner.

  • Slack Packaging Progress Notification.

  • Mobile Support

  • Support restricting account usage within organizational teams

Main Responsibilities

0 state, 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.

Operation Requirements: Supports both mobile phones and computers.

Permission Requirement: Access should be restricted to team organization members only.

Online Demo Web App

Sign in
Edit description script.google.com

Technology Choices

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

Integration into Slack

We tried using Slack as the packaging platform, developing our own backend service hosted on GCP, integrating with Slack API and Asana API, then forwarding 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 painless use; the downside is the high development and maintenance cost. Since the backend service was developed using Ktor, app engineers had to also handle backend tasks, manage Google service OAuth integration issues, and implement some features here (e.g., submission for review)… making it complex. If new team members later fail to take over this part properly, maintenance becomes nearly impossible. Additionally, there is a monthly $15 USD GCP server fee.

In the early stages, I also tried using FaaS services to connect with the Slack API, such as Cloud Functions. However, the Slack API requires the connected service to respond within 3 seconds; otherwise, it is considered a failure. FaaS has the cold start issue, where the service goes to sleep after a period of inactivity and takes longer to respond (≥ 5 seconds) on the next call. This causes the Slack form submission to be unstable, frequently resulting in Timeout Errors.

Integration into Internal Systems

This is obviously the optimal solution. If the team has web and backend developers, directly integrating with the existing system is the best and safest approach.

The premise of this article is: None, the app stands on its own.

Google Apps Script — Web App

Google Apps Script has been our old partner. We have used it in 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 reminded me of it again; GAS has a feature to deploy as a Web (App) directly as a web service.

Advantages of Google Apps Script:

  • ✅ Free, hardly any limits under normal use

  • ✅ Functions as a Service means no need to set up or maintain servers yourself

  • ✅ Permission control is the same as Google Workspace, allowing usage only by Google accounts within the organization

  • ✅ Seamlessly integrate Google ecosystem services (e.g. Firebase, GA, etc.) and data (no need to handle OAuth yourself)

  • ✅ Programming language uses JavaScript, easy to learn (V8 Runtime supports ES6+)

  • ✅ Fast writing, fast launch, fast usage

  • ✅ Stable and long-lasting service (launched over 16 years ago)

  • ✅ AI Can Help! Testing ChatGPT-Assisted Development Achieves Up to 95% Accuracy

Disadvantages of Google Apps Script:

  • ❌ Built-in version control is difficult to explain clearly

  • ❌ No built-in support for file, data storage, or key/certificate management

  • ❌ Web App cannot achieve a fully responsive design experience

  • ❌ Projects can only be linked to personal accounts, not organizations

  • ❌ Although Google continues to develop and maintain it, overall feature updates are slow

  • ❌ Network request UrlFetchApp does not support setting the User-Agent

  • ❌ Web App doGet / doPost does not support retrieving header information

  • ❌ FaaS Cold Start Issue

  • No support for multiple developers working simultaneously
    However, this has little impact on the Web App; at most, you just need to wait a few extra seconds to access the webpage.

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

Also only applicable to teams using Google Workspace work environment; Balancing resources and needs, Google Apps Script — Web App was chosen to implement the packaging tool platform.

UI Framework

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

Hands-on Practice

The entire platform architecture has been open-sourced here. Everyone can customize it based on their team’s needs using this version.

Open Source Sample Projects

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 simple pseudo-MVC architecture. If you want to adjust it or don’t understand a feature, you can ask AI for accurate answers.

System

  • appsscript.json: Metadata file for GAS system
    The key point is the “oauthScopes” variable, which declares the external permissions this Script will use.

  • Entrypoint.gs: Define the doGet() entry point

Controller

  • Controller_iOS.gs: Controller for the iOS packaging tool page, responsible for fetching data for the View display

View

  • View_index.html: The entire packaging tool skeleton and 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: Define key content
    (⚠️️️Please note️, using GCP IAM with GAS is quite complex, so we directly define the key here. Therefore, this GAS project contains confidential information. Do not share project view or edit permissions casually.)

  • StubData.gs: Stub methods and data for Online Demo.

  • Settings.gs: Some common settings and library initialization.

  • GitHub.gs: GitHub API operation wrapper.

  • Slack.gs: Slack API operation wrapper.

  • Firebase.gs: Firebase — App Distribution API operation wrapper.

Build 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 and create all files according to the example, then copy the content directly.

Stupid, but no choice.

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

Another method is to use clasp (Google Apps Script CLI) to git clone the demo project and then push the code.

After copying, it will look exactly the same as the sample project.

  1. First Deployment of the “Web Application” to View Results

Top right corner of the project: “Deploy” → “New deployment” → Type: “Web app”:

Execution Identity:

  • I always run the scripts using your account identity.

  • The user accessing the web application will execute the script using the currently logged-in Google account identity.

Who can access:

  • Only myself

  • XXX All users within the same organization Only users with the same organization + logged-in Google accounts can access.

  • All users signed in to a Google account Users signed in to Google accounts can access.

  • Everyone does not need to log in to a Google account, and everyone can access it publicly.

If it is an internal tool: you can choose “Who can access: All users in the same organization as XXX” + “Run as: User accessing the web app” for security control.

The URL of the “web application” after deployment is your Web App packaging tool URL, which you can share with your team members. (The URL may look ugly, so you can use a URL shortening service to tidy it up. Updating the deployment content will not change the URL.)

User must agree to authorization on first use

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

  • Review Permission → Select the account identity to use this Web App

  • Unverified warning window, click “Advanced” to expand → Click Proceed to “XXX” (unsafe)

  • Click “Allow”

If the script permissions remain unchanged, reauthorization is not required.

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

Demo packaging tool deployed successfully 🎉🎉🎉

Note: The prompt “This app was created by a Google Apps Script user” cannot be automatically hidden.

Update Deployment

⚠️ All code changes require updating and deployment to take effect.

⚠️ All code changes require updating and deployment to take effect.

⚠️ All code changes require updating and deployment to take effect.

Note that saving code changes does not directly affect the Web App. If you find that refreshing has no effect, this is the reason; you need to go to “Deploy” → “Manage Deployments” → “Edit” → create a new version → click “Deploy” → “Done”.

Refresh the webpage after updating the deployment to see the changes take effect.

Added test deployment for easier development

As mentioned earlier, all changes require an update deployment to take effect; this is very troublesome during the development phase. Therefore, during development, we can use a “test deployment” to quickly verify if the changes are correct.

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

During development, we can directly use this URL to save. After modifying and saving the file, refresh the page at this development URL to see the results!

After all development is complete, update and deploy as described earlier, then release it to users.

Modify the Demo Sample Project to Connect Real Data

Next is the key point: integrating real data. The GitHub Actions Workflow refers to the CI/CD process established in the previous article. You can also adjust the parameters according to your actual Actions Workflow.

⚠️ Please Note Before Editing

The Google Apps Script platform does not support multi-user or multiple window development well. A pitfall I encountered was accidentally opening two editing windows. After editing in window A, I continued editing in window B, which caused all changes to be overwritten by B’s older version. Therefore, it is recommended that only one person edits the script in one window at a time.

GitHub Integration

Insert 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 date).

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

  • Repo: Remember to select the Repo to operate on

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

If you don’t want to rely on someone’s account, it is recommended to create a dedicated team GitHub account and use its Token.

Go to the GAS project → Credentials.gs → Insert the token into the githubToken variable.

Replace GithubStub with GitHub:

Go to the GAS project → Settings.gs → then:

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

Change to

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

Save the file.

Refresh the test “web application” URL to check if the changes are correct:

Correctly displaying data means: GitHub successfully linked real data 🎉🎉🎉

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

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

Slack Integration

To connect Slack notifications, we first need to go back to the Repo → GitHub Actions and add a notification container Action that wraps the package bundling 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 for Actions Log
run-name: "[CD-Deploy-Form] ${{ github.ref }}"

# Cancel running jobs in the same Concurrency Group if a new job starts
# For example, repeated triggers of the same branch packaging task will cancel the previous one
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
      # Triggering user's Slack User ID
      SLACK_USER_ID:
        description: 'Slack user id.'
        required: true
        type: string
      # Triggering user'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 task runs directly on GitHub Hosted Runner, low usage
    runs-on: ubuntu-latest
    
    # Set maximum timeout to prevent endless waiting in abnormal cases
    # Normally should not exceed 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: "Packaging request received.\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
    # ts = Slack message ID, used for replying in the same thread later
    outputs:
      ts: ${{ steps.slack.outputs.ts }}

  deploy:
    # Jobs run concurrently by default, use needs to wait for start-message to finish before running
    # Execute packaging and 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 and 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 and deployment succeeded.\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"

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

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

Full code: CD-Deploy-Form.yml

This Action is just a container that connects Slack notifications. It actually reuses the CD-Deploy.yml Action written in the previous article.

  • For setting up a Slack Bot App and message permissions, you can refer to my previous article.

  • Remember to add the corresponding SLACK_BOT_TOKEN in Repo → Secrets and input the Slack Bot App Token value.

Return to the GAS project → Credentials.gs → Insert the token into the slackBotToken variable.

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

1
const slack = new SlackStub(slackBotToken);

Change to

1
const slack = new Slack(slackBotToken);

Save the file.

If you don’t have a ready Slack Bot App for sending notifications and don’t want to create one, you can skip all the steps here and delete any Slack-related code in the GAS project.

GitHub Integration — Packaging Forms

Go to GAS project → Controller_iOS.gs → adjust View_iOS_Form.html content:
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 (currently main).

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

  • Remove the line template.tasks = Stubable.fetchStubAsanaTasks(); which stubs the Asana connection.
    If you want to connect to Asana/Jira, you can directly ask ChatGPT to help generate the integration method.

  • template.prs = iOSGitHub.fetchOpenPRs(); really connects to the GitHub API to get the list of opened PRs and can be kept as needed.

Post-Submission Processing Inside iOSSubmitForm():

Adjust according to the actual GitHub Actions Workflow file name and the workflow_dispatch inputs 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 field validation. Here, it only checks that the branch must be filled; otherwise, an error message will appear.

If you think this is not secure enough, you can add password verification or restrict access to specific accounts.

The last line Slack notification requires proper Slack setup. If you don’t have a Slack Bot App or don’t 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 packaged form only has the Opened PR List left.

Fill in the information and click “Submit Request” to test the packaged form:

Successful prompt submission means no issues. You can also see the task starting in the packaging records 🎉:

Repeatedly tapping the package record can update the progress.

Common Submission Errors:

Required input ‘SLACK_USER_ID’ not provided : The SLACK_USER_ID field is required but not provided. This may be due to Slack configuration failure or the current User Email not matching any Slack UID.

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

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

The last small feature is integrating Firebase App Distribution to directly obtain download information and links, making it easy to open the packaging platform tool on the phone and click to download and install directly.

Previously introduced “Google Apps Script x Google APIs Quick Integration Method“—GAS can quickly and easily integrate with Firebase.

Integration Principle

Before integration, let’s first explain the integration principle of this “Tricky.”

Our packaging platform has no database and acts purely as an API relay; so in practice, when we run the packaging job in GitHub Actions CD-Deploy.yml, we include the Job Run ID in the Release Note (of course, it can also be included in 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 }}"

# Compose 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 include the Job Run ID.

The GAS Web App packaging tool platform connects to the GitHub API to get GitHub Actions run records. We directly use the Job Run ID from the API to query the Firebase App Distribution API. If the Release Notes contain *ID: XXX*, we can find the corresponding packaging record.

Mapping between two tool platforms without using any database.

Project Integration Settings

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

Enter the Firebase project ID you want to connect.

Initial setup may show errors “To change the project, please set up the OAuth consent screen. Provide detailed information for the OAuth consent screen.” If not applicable, you can skip the following steps.

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

Click “Start”:

App Information:

  • App Name: Enter Your Tool Name

  • User Support Email: Select Email

Target Audience:

  • Internal: For use by organization partners only

  • External: Available to all Google account users after they consent to authorization

Contact Information:

  • Enter Email to Receive Notifications

Check to agree to the 《 Google API Services: User Data Policy 》。

Finally, click “Create”.

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

Enter the Firebase project number again and click “Change Project.”

No errors mean the binding is complete.

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

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

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

Users can complete the authorization by following the “User must agree to authorization on first use” step mentioned earlier and then start using it!

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

Access to "XXX" Blocked Due to Incomplete Google Verification

Access to “XXX” blocked due to incomplete Google verification process

— — —

Integrating the Project

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

Only go to the GAS project → Settings.gs → and replace:

1
const iOSFirebase = new FirebaseStub(iOSFirebaseProject);

Change to

1
const iOSFirebase = new Firebase(iOSFirebaseProject);

Done.

Refresh the test “web application” URL to the packaging record → Find a record and click “Get Download Link”:

If the corresponding Job Run Id package is found in Firebase App Distribution Release Notes, the download information will be displayed directly, and clicking download will take you straight to the download page.

Completed! 🎉🎉🎉

Results

By now, you have replaced the examples with your actual usable packaging tools. The remaining customization features, additional third-party API integrations, and more forms can be extended on your own (discuss with ChatGPT).

Finally, don’t forget that after development and testing are complete, you need to go live by following the previous steps — only then will the update deployment take effect!

Integration Extension

Continuing our role as a “relay station,” 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
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 — 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 condition
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 a database is really needed, Google Sheets can be used as a substitute:

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 & Message Sending Methods:

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 and participation. The CI/CD from 0 to 1 series ends here; I hope it has helped you and your team build a complete CI/CD workflow to improve 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 them helpful, please follow me on Medium and share with your friends and colleagues.

You’re welcome.

Buy me a coffee

This series of articles took a lot of time and effort to write. If the content helps you or improves your team’s work efficiency and product quality, please consider buying me a coffee. Thank you for your support!

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

Buy me a coffee

Series Articles:

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


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

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