Post

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

Developers facing complex CI/CD setups can streamline workflows by connecting Google Apps Script Web App with GitHub Actions, Slack, and Firebase APIs to build a free, collaborative packaging tool platform that enhances cross-team efficiency.

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!

Table of Contents


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

GAS Web App Integrates GitHub, Slack, Firebase, or Asana/Jira APIs to Build a Middleware Platform for Cross-Team Shared Packaging Tools

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 App iOS CI and CD Workflows with GitHub Actions, we completed the foundational CI/CD setup for the iOS app project. Now, CI can automatically run tests and CD can handle packaging and deployment. However, in real-world product development, packaging and deployment are often done to deliver builds to other functional teams for QA (Quality Assurance) testing. At this point, the CD scenario extends beyond engineering and 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 provides a simple form for users to run builds. However, it is not user-friendly for non-engineers who may not understand: what is a branch? Should fields be filled? How to know when the build is done? How to download it once ready? …etc.
Additionally, there is a permission control issue. To allow other team members to use GitHub Actions for building, their accounts must be added to the repo. This is insecure and unreasonable from a security perspective, as they get access to the entire source code just to operate the build form.

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

`workflow_dispatch form style`

Form Style of workflow_dispatch

Therefore, we need an intermediary packaging platform to serve other functional users, integrating Asana/Jira task tickets so users can package the app directly using the task ticket and view progress and download results on it.

GAS Web App Relay Station

GAS Web App as a 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 Result Screenshot

  • Packaging Form: Integrate project management tools to fetch ticket numbers and GitHub to retrieve open Pull Requests

  • Build Records: Displays build history, progress status of ongoing build tasks, and allows clicking to retrieve download links from Firebase App Distribution.

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

  • Slack build progress notification.

  • Mobile Support

  • Support restricting usage to accounts within the organization team

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: Support both mobile and desktop devices.

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

Online Demo Web App

  • For first-time use, please refer to the authorization in the image below (Only For Demo App):

Project source code: https://script.google.com/home/projects/1CBB39OMedqP9Ro1WSlvgDnMBin4-ksyhgly2h_KrbOuFiPHTalNgwHOp/edit

Technology Choices

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

Integration with Slack

We tried using Slack as the packaging platform by developing our own backend service hosted on GCP, integrating with Slack API and Asana API, then forwarding the forms submitted via Slack to the GitHub API to trigger subsequent GitHub Actions. The experience was very smooth and unified within the team’s collaboration tool, allowing painless usage. The downside was the high development and maintenance costs. Since the backend service was built with Ktor, app engineers had to handle backend tasks, manage Google service OAuth integration, and implement some features (e.g., submission for review) there, making it complex. If new team members didn’t properly take over this part later, it became almost impossible to maintain. Additionally, there was a monthly $15 USD GCP server cost.

Initially, we also tried using FaaS services to connect with the Slack API, such as Cloud Functions. However, the Slack API requires the service to respond within 3 seconds, or it considers the request failed. FaaS has a cold start problem, where the service goes to sleep after a period of inactivity and takes longer to respond (≥ 5 seconds) when called again. This caused the Slack packaging form to be very 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 is self-reliant.

Google Apps Script — Web App

Google Apps Script has been our longtime partner. We have 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 Automate Daily Data Report RPA”. At this point, I recalled it again; GAS has a feature to deploy as a Web (App) to serve directly as a web service.

Advantages of Google Apps Script:

  • ✅ Free, hardly reaches limits under normal usage

  • ✅ Functions as a Service means no need to set up or maintain your own server

  • ✅ Permission control integrates with Google Workspace, allowing use only by Google accounts within the organization

  • ✅ Seamless integration with Google ecosystem services (e.g., Firebase, GA, etc.) and data (no need to implement OAuth yourself)

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

  • ✅ Fast writing, fast deployment, fast usage

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

  • ✅ AI Can Help! Tested Using ChatGPT to Assist Development, Accuracy Can Reach 95%

Google Apps Script Disadvantages:

  • ❌ Built-in version control is difficult to describe positively

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

  • ❌ Web App cannot fully achieve a 100% responsive web design (RWD) experience

  • ❌ The project can only be linked to a personal account, not an organization

  • ❌ Although Google continues development and maintenance, overall feature updates are slow

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

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

  • ❌ FaaS Cold Start Issue

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

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 a Slack-based solution, this approach is faster, lighter, and easier to hand over and get started with. The drawbacks are that the team needs to know the tool’s URL and how to use it, and due to GAS library limitations (e.g., no built-in encryption algorithms), it can basically only serve as a pure relay platform, such as submitting for review, which only forwards the review request to GitHub Actions.

Also only suitable for teams using Google Workspace; 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 Project

View the project directly on GAS:

GitHub Repo Backup:

File Structure

I wrote a very simple class-based MVC-like structure. If you need adjustments or don’t understand any feature, 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: Define the doGet() entry point

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 framework 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 contents
    (⚠️ Please note, using GCP IAM with GAS can be quite complex, so we directly define the keys here. Therefore, this GAS project contains sensitive information; do not share project view or edit permissions casually.)

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

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

  • GitHub.gs: Wrapper for GitHub API operations.

  • Slack.gs: Wrapper for Slack API operations.

  • Firebase.gs: Firebase — Wrapper for App Distribution API operations.

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 make the “appsscript.json” metadata file appear.

  1. Refer to my open source project files, create all files following the examples, and blindly copy the content over.

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 example project.

  1. First Deployment of the “Web App” and Viewing Results

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

Execution Identity:

  • I execute the script always using your account identity.

  • Users accessing the web app will run the script as the currently logged-in Google account user.

Who can access:

  • Only me

  • All users within the same organization Only users logged in with a Google account from the same organization can access.

  • All users logged into a Google account All logged-in Google account users can access.

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

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

The URL of the “Web App” after deployment is your Web App packaging tool link, which you can share with your team members. (The URL may look ugly, so you can use a URL shortener 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 authorize access.

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

No need to reauthorize if the script permissions remain unchanged.

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

Demo packaging tool deployed successfully 🎉🎉🎉

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

Update Deployment

⚠️ All code changes require redeployment to take effect.

️️⚠️ All code changes require redeployment to take effect.

⚠️ All code changes require redeployment to take effect.

Note that saving code changes does not directly update the Web App, so if 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 page after updating the deployment to see the changes take effect.

Add test deployment for easier development

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

Go to “Deploy” → “Test deployments” → obtain the test “Web App” URL.

During development, we can save directly using this URL. After saving file changes, 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 for users to use.

Modify the Demo Sample Project to Connect with Real Data

Next is the key part: connecting 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 Making Changes

The Google Apps Script platform does not support multi-user or multiple windows development well. From my experience, if you accidentally open two editing windows, edit in window A first, then later edit in window B, the changes from A will be overwritten by the older version in B. Therefore, it is recommended that only one person edits the script in one window at a time.

GitHub Integration

Inject 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 (though they have an expiration).

The required permissions 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 don’t want to rely on someone’s personal 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 → change:

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

Change to

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

Save the file.

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

Correctly displaying data means: GitHub webhook successfully connected with real data 🎉🎉🎉

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

Note: My Runner is off… so it’s offline.

Slack Integration

To integrate Slack notifications, we first need to go back to the Repo → GitHub Actions and add a notification wrapper Action for the package build 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 }}"

# Cancel running jobs in the same concurrency group if a new job starts
# For example, if the same branch's packaging job is triggered repeatedly, the previous job will be cancelled
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# Trigger events
on:
  # Manual form trigger
  workflow_dispatch:
    # Form input 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:
    # Run small jobs directly on GitHub Hosted Runner, usage is low
    runs-on: ubuntu-latest
    
    # Set maximum timeout to prevent endless waiting in abnormal situations
    # 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 to reply in the same thread later
    outputs:
      ts: ${{ steps.slack.outputs.ts }}

  deploy:
    # Jobs run concurrently by default; needs ensures this waits for start-message to finish
    # 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
  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
  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 results or try again later.\n\ncc'ed <@${{ inputs.SLACK_USER_ID }}>"

  # Cancelled message for packaging and deployment
  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, actually reusing the CD-Deploy.yml Action written in the previous article.

  • For setting up the Slack Bot App and message sending permissions, please refer to my previous article.

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

Back to the GAS project → Credentials.gs → Assign the Token to the slackBotToken variable.

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 to send notifications and don’t want to create one, you can skip all the steps here and remove the Slack-related usage from the GAS project.

GitHub Integration — Build Form

Go to the GAS project → Controller_iOS.gs → modify 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 → modify the content of iOSLoadForm():

  • Remove the line template.tasks = Stubable.fetchStubAsanaTasks(); which is a stub method for Asana integration.
    If you want to integrate Asana/Jira, you can directly ask ChatGPT to help generate the integration method.

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

Post-submission Processing Inside iOSSubmitForm():

You can 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 validations. 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 App” URL to check if the changes are correct:

You can see that only the Opened PR List remains in the packaging form.

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

If the prompt submission is successful, it means there is no problem. You can also see the task started in the packaging records 🎉

Repeated packaging records can update progress.

Common Submission Errors:

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

Workflow does not have ‘workflow_dispatch’ trigger , Branch is outdated, please update the 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: The branch could not be found.

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

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

Our build platform has no database and only serves as an API relay; so in practice, when we run the build 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 }}"

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

# Run Fastlane build & deploy 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 a version with *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 configure the OAuth consent screen. Details on setting up 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”:

Application Information:

  • Application Name: Enter Your Tool Name

  • User Support Email: Select Email

Target Audience:

  • Internal: For use by organization members only

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

Contact Information:

  • Email for receiving notifications

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

Finally, click “Create”.

Back 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 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 “Deploy as Web App” → Done.

Users can start using it after completing the authorization following the “First-time user authorization” steps mentioned above!

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

Access Denied "XXX" Google Verification Process Not Completed

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

— — —

Project Integration

Back to the integration, Firebase directly uses ScriptApp.getOAuthToken() to dynamically obtain the token based on the execution identity, so no token setup is needed.

Just go to the GAS project → Settings.gs → and change:

1
const iOSFirebase = new FirebaseStub(iOSFirebaseProject);

Change to

1
const iOSFirebase = new Firebase(iOSFirebaseProject);

Done.

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

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

Done! 🎉🎉🎉

Results

[Demo Web App](https://script.google.com/macros/s/AKfycbwNW6N5ozKbIz_E1HK6yFEUtA8KQrUciS-jcPsQptvIKlARmKgLxbQzNu8ksVeg-BmEfg/exec){:target="_blank"}

Demo Web App

By now, you have replaced the examples with your actual usable packaging tools. Remaining custom 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 follow the previous steps — updating the deployment for the changes to take effect!

Integration Extensions

Continuing the spirit of our “middleman” 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
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 & Sending Message Method:

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 helps you and your team build a complete CI/CD workflow to improve efficiency and product stability. Feel free to leave comments if you have any implementation questions. 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 and 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 suggestions, 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.