Google Apps Script Web App|Integrate Forms with GitHub Actions CI/CD for Streamlined Workflows
Developers facing workflow inefficiencies can automate GitHub Actions CI/CD triggers using Google Apps Script Web App forms, seamlessly integrating tools like Jira, Asana, and Slack to boost productivity and collaboration.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
Using Google Apps Script Web App Form to Integrate with Github Action CI/CD Workflow
Github Action Workflow form optimization and integration with other workflow tools (Jira, Asana, Slack, etc.) to enhance development efficiency.
Left: Original Github Action Workflow Form / Right: Final Result (GAS Web App Form)
2025/07 Update:
This feature has been integrated into the actual packaging tool. Please refer to the latest article example: “CI/CD Practical Guide (Part 4): Using Google Apps Script Web App to Connect GitHub Actions for Building a Free and Easy Packaging Tool Platform”
Background
The previous team used Github Action & Self-hosted Github Runner + Slack to build a complete CI/CD service; the overall effect was good. For app developers, setup and maintenance were relatively easy. Just follow the official documentation to configure the YAML parameters, and it would trigger automatically. On the machine side, it was easy to use your own machine as a Runner. The service itself is maintained by Github, so we didn’t need to worry about version upgrades. The Runner requests tasks from Github, so there’s no need to open external network ports. Even better for the team, using your own Runner to run Github Action is completely free.
It means enjoying a Bitrise-like GUI YAML build method while having the flexibility and lower build costs of using self-hosted machines like Jenkins, without the need to spend time maintaining the service itself as Jenkins requires.
Will write a complete article on App CI/CD setup with Github Action when I have time in the future.
Question: Github Action CI/CD GUI Form
Github Action GUI Form
In app development, when CD triggers packaging for test versions, official releases, or submissions for review, it usually requires providing some external parameters or selecting environments and branches based on needs before starting the process.
Unlike Jenkins, which is a self-hosted service with a complete Web GUI, GitHub Actions does not have one. The only Web GUI form is found by clicking “Run workflow” in Actions, allowing users to customize a simple form to input external parameters and trigger the CI/CD workflow.
Users who typically use this CD packaging are not necessarily the app developers themselves, nor do they always have project permissions; for example, QA needs to package a specific version, PM/Backend needs to package a development version for testing. Github Action Form requires project permissions to use, but users may not have project permissions or even an engineering background.
And we cannot create dynamic forms or data validation here.
Therefore, we need to set up a separate GUI service for other users to operate.
Building a Custom Slack App Solution
Previously, a team member passionate about automation built a complete Slack App web service using Kotlin+Ktor. It integrated Slack messages, forms, commands, and other features to receive and forward CD packaging requests, trigger Github Actions, and send the results back to Slack.
Currently, there are no development resources, so the service is built using Kotlin+Ktor as before.
Writing Your Own Web/iOS/macOS App Tools
The team originally used Jenkins, which has a basic web interface for other users to log in and use. Additionally, they developed an app that connects to Jenkins, encapsulating some parameters to make it easier for users without an engineering background to use.
However, after migrating to Github Actions, this whole setup was abandoned.
❌ Private Github Pages
There should be a chance to directly build Github Pages as a CI/CD Web GUI, but currently only Github Enterprise allows setting access permissions for Github Pages. Other plans, even with Private Repos, will be public; there is no security.
❌ Slack App, but built with Google Apps Script
At first, I planned to use the Slack App as the CI/CD GUI Form service based on the team’s previous experience. However, we currently lack the resources to build the service with Kotlin+Ktor as before. So, I decided to quickly try building it using a Function as a Service.
There are many types of Function as a Service. Cloud Functions offer higher flexibility, but due to organizational IT restrictions, we cannot freely add Public Cloud Functions and there are cost concerns; therefore, we return to our old friend — Google Apps Script.
I have previously written several articles about automation using Google Apps Script. Interested readers can refer to them:
1. “Automate Daily Data Reports with Google Apps Script RPA“
2. Simple 3 Steps — Build a Free GA4 Automated Data Notification Bot
3. “Crashlytics + Google Analytics Automatic Query for App Crash-Free Users Rate”
4. “Crashlytics + Big Query Build a More Real-Time and Convenient Crash Tracking Tool “
In summary, Google Apps Script is another Function as a Service offering from Google, mainly featuring free usage and quick integration with Google services. However, it has several limitations, such as using only its own language, a maximum execution time of 6 minutes, limits on the number of executions, no support for multithreading, and more. For details, please refer to my previous article.
The conclusion is not feasible because:
Function as a Service Cold Start Issue.
If the service is idle for a while, it goes to sleep, causing the next call to take longer to start (3 to ≥ 5 seconds). Slack Apps require very strict API response times; the service must respond within 3 seconds or it is considered a failure. Slack will then throw an error, and event listening is lost, leading to repeated sends.Google Apps Script doGet and doPost methods cannot access Headers.
This prevents using official security verification and disables the ability to turn off Slack Retry.Google Apps Script single-thread issue.
If the response time to connect to other services exceeds 3 seconds, Slack will directly mark it as a failure.
I tried to forcefully use Slack messages, Block Kit, and Forms to connect the entire process, but the above issues occurred too frequently, so I eventually gave up.
If you want to do this setup, you still need to run your own server and services, not use Function as a Service! !
❌ Slack Workflow Form
Slack Workflow Form (❌ No customization available)
I also tried Slack’s built-in automation feature, Workflow Form, but it cannot create dynamic form content (e.g., fetching branches for users to select). The only customizable part is the subsequent data submission step.
✅ Google Apps Script Web App GUI Form
If the mountain doesn’t turn, the road will. On second thought, we don’t have to insist on integrating with Slack. Integrating with Slack is the best solution because it directly connects with the existing team collaboration tool, avoiding the need to learn new tools. However, due to resource constraints, we have to settle for other stable and easy-to-use methods.
Looking back, Google Apps Script itself can be deployed as a Web App, responding with a GUI Form during Web doGet, and triggering subsequent GitHub integration after the form is submitted.
Final Results 🎉
Workflow
We use Google Apps Script Web App to build a CI/CD form, directly linked to Google Workspace accounts, restricting access to users within the organization only. It automatically retrieves the current logged-in user’s email, uses a shared Github Repo account (or borrows a permissioned account) Personal Access Token to call the Github API to get the branch list, and upon submission, calls the API again to trigger Github Actions to start the CI/CD process.
Additionally, we can use the user’s email to call the Slack API through the Slack App to get the user’s Slack ID, then send a message via the Slack App to notify the CI/CD task execution status.
It can also be integrated with other tools and development workflows, such as retrieving tasks from Asana or Jira, selecting them, then using the Github API to find branches and trigger Github Actions, and finally notifying users through Slack.
Step 1. Create Google Apps Script Web App Form
Go to > Google Apps Script and create a new project.
Step 2. Create Form Content and GAS Script
It’s been a long time since I wrote HTML and CSS, and I’m too lazy to design the style myself, so I directly asked ChatGPT to generate a slightly styled HTML form template.
In the GAS file list on the left, click “+” to add a new file, enter the file name Form.html
, and paste the GPT-generated HTML form template content.
Form.html:
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
135
136
<!--HTML & Style Gen by ChatGPT 4o-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?=title?></title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f7f7f7;
}
.form-container {
max-width: 600px;
margin: auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.form-container h2 {
margin-bottom: 20px;
color: #333333;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555555;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 95%;
padding: 10px;
border: 1px solid #cccccc;
border-radius: 4px;
font-size: 16px;
}
.form-group input[type="radio"] {
width: auto;
margin-right: 10px;
}
.form-group .radio-label {
display: inline-block;
margin-right: 20px;
}
.form-group button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.form-group button:hover {
background-color: #45a049;
}
.message {
margin-top: 20px;
padding: 15px;
border-radius: 5px;
font-size: 1em;
text-align: center;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="form-container">
<h2><?=title?></h2>
<form id="myForm">
<div id="message-block" class="hidden"></div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" value="<?=email?>" readonly/>
</div>
<div class="form-group">
<label for="buildNumber">Build Number:</label>
<input type="number" value="<?=buildNumber?>"/>
</div>
<div class="form-group">
<label for="branch">PRs Under Review:</label>
<select id="branch" name="branch">
<option>Please select</option>
<? pullRequests.forEach(pullRequest => { ?>
<option value="<?=pullRequest.head.ref?>">[<?=pullRequest.state?>] <?=pullRequest.title?></option>
<? }); ?>
</select>
</div>
<div class="form-group">
<label for="message">Update Details:</label>
<textarea id="message" name="message" rows="4" placeholder="Please enter your message"></textarea>
</div>
<div class="form-group">
<button type="submit">Submit</button>
</div>
</form>
</div>
<script>
function displayMessage(ok, message) {
const messageBlock = document.getElementById('message-block');
messageBlock.className = ok ? 'message success' : 'message error';
messageBlock.innerHTML = message;
messageBlock.classList.remove('hidden');
}
document.getElementById("myForm").addEventListener("submit", function(e) {
e.preventDefault();
const formData = new FormData(this);
const formObject = Object.fromEntries(formData);
google.script.run.withSuccessHandler((response) => {
displayMessage(response.ok, response.message);
}).processForm(formObject);
});
</script>
</body>
</html>
The form content can be adjusted according to your needs.
code.gs:
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
function doGet(e) {
// Corresponds to the file Form.html on the left
const htmlTemplate = HtmlService.createTemplateFromFile('Form');
const email = Session.getActiveUser().getEmail();
// Get user email, valid only for execution identity: user accessing the web app
const title = "App CD Packaging Request Form";
const buildNumber = genBuildNumber();
htmlTemplate.email = email;
htmlTemplate.title = title;
htmlTemplate.pullRequests = []; // Next step: connect to Github...
htmlTemplate.buildNumber = buildNumber;
const html = htmlTemplate.evaluate();
html.setTitle(title);
//html.setWidth(600) // Set page width
return html
}
function processForm(object) {
return {"ok": true, "message": "Request submitted successfully!"};
}
function genBuildNumber() {
const now = new Date();
const formattedDate = Utilities.formatDate(now, "Asia/Taipei", "yyyyMMddHHmmss");
const milliseconds = now.getMilliseconds().toString().padStart(3, '0'); // Ensure milliseconds are 3 digits
return `${formattedDate}${milliseconds}`;
}
In this step, we first complete the form GUI. Next, we will connect to the Github API to get the list of PR branches.
Step 2. Deploy Google Apps Script Web App Form
Let’s first deploy the previous content and check the results.
In the top right corner of GAS, select “Deploy” -> “New deployment” -> “Web app”:
Execution identity and access permissions can be set as:
Execution Identity:
I
always run the script 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 Me
XXX All users within the same organization
Only users from the same organization with a logged-in Google account can access.
All users signed in to a Google account
All signed-in Google account users can access.
Everyone
does not need to sign in to a Google account, and everyone can access it publicly.
We choose “Who can access: XXX All users in the same organization” + “Execution identity: The user accessing the web app” to automatically restrict usage to only organization accounts, and run it under their own identity!
It is a very convenient feature for permission management!
After selecting, click “Deploy” at the bottom right.
The URL in a web application is the access address for the Web App.
1
https://script.google.com/macros/s/AKfycbw8SuK7lLLMdY86y3jxMJyzXqa5tdxJryRnteOnNi-lK--j6CmKYXj7UuU58DiS0NSVvA/exec
The URL is very long and ugly, but there’s no choice; you have to find a URL shortener tool to shorten it.
Click the link to open the page and see the effect:
Here are two more GAS limitations to mention:
GAS Web App Top Warning Message Cannot Be Hidden by Default
GAS Web App uses an IFrame to embed our page, making it difficult to achieve a 100% responsive design.
We can only use.setWidth()
to adjust the window width.
Google Apps Script Authorization Warning
First Time Use: Clicking “Debug” or “Run” may trigger the following authorization warning:
Select the account you want to use. If you see “This app isn’t verified by Google,” click “Advanced” -> “Go to XXX (unsafe),” then choose “Allow”:
If the GAS script permissions change (e.g., adding access to Google Sheets, etc.), re-authorization is required. Otherwise, once authorized, the prompt will not appear again.
If you encounter: Access to “XXX” is blocked due to incomplete Google verification process, please refer to my latest article on GCP settings.
Step 3. Connect to Github API to Get PR Branch List
We add a new Github.gs
file to store the logic related to the Github API.
Github.gs:
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
// SECRET
const githubPersonalAccessToken = ""
// Create a PAT using your Github account or a shared Github account for your organization
// https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
// Method 1: Access via Restful API
function githubAPI(method, path, payload = null) {
try {
const url = "https://api.github.com"+path;
var options = {
method: method,
headers: {
"Accept": "application/vnd.github+json",
"Authorization": `Bearer ${githubPersonalAccessToken}`,
"X-GitHub-Api-Version": "2022-11-28"
}
};
if (method.toLowerCase().trim() == "post") {
options.payload = JSON.stringify(payload);
}
const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());
return data;
} catch (error) {
throw error;
}
}
// Method 2: Access via GraphQL
// Some more detailed query features of Github API are only available via GraphQL API
// https://docs.github.com/en/graphql
function githubGraphQL(query, variables) {
const url = "https://api.github.com/graphql";
const payload = {
query: query,
variables: variables
};
const options = {
method: "post",
contentType: "application/json",
headers: {
"Accept": "application/vnd.github+json",
"Authorization": `Bearer ${githubPersonalAccessToken}`,
"X-GitHub-Api-Version": "2022-11-28"
},
payload: JSON.stringify(payload)
};
try {
const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());
return data;
} catch (error) {
throw error;
}
}
// GraphQL Example:
// const query = `
// query($owner: String!, $repo: String!) {
// repository(owner: $owner, name: $repo) {
// pullRequests(states: OPEN, first: 100, orderBy: { field: CREATED_AT, direction: DESC }) {
// nodes {
// title
// url
// number
// createdAt
// author {
// login
// }
// headRefName
// baseRefName
// body
// }
// pageInfo {
// hasNextPage
// endCursor
// }
// }
// }
// }
// `;
// const variables = {
// owner: "swiftlang",
// repo: "swift"
// };
// const response = githubGraphQL(query, variables);
Github API has two access methods: the traditional Restful and the more flexible GraphQL; this article uses Restful as an example.
程式碼.gs:
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
function doGet(e) {
// Corresponds to the file Form.html on the left
const htmlTemplate = HtmlService.createTemplateFromFile('Form');
const email = Session.getActiveUser().getEmail();
// Get the user's email, valid only for execution identity: user accessing the web app
const title = "App CD Packaging Request Form";
const pullRequests = githubAPI("get", "/repos/swiftlang/swift/pulls");
// Example: https://github.com/swiftlang/swift/pulls
const buildNumber = genBuildNumber();
htmlTemplate.email = email;
htmlTemplate.title = title;
htmlTemplate.pullRequests = pullRequests;
htmlTemplate.buildNumber = buildNumber;
const html = htmlTemplate.evaluate();
html.setTitle(title);
//html.setWidth(600) // Set page width
return html
}
function processForm(object) {
if (object.buildNumber == "") {
return {"ok": false, "message": "Please enter the build number!"};
}
if (object.branch == "") {
return {"ok": false, "message": "Please select a branch version!"};
}
// Pass the parameters you want to send to Github Action
const payload = {
ref: object.branch,
inputs: {
buildNumber: object.buildNumber
}
};
//
try {
const response = githubAPI("post", "/repos/zhgchgli0718/ios-project-for-github-action-ci-cd-demo/actions/workflows/CD-Job.yml/dispatches", payload);
// Example: https://github.com/zhgchgli0718/ios-project-for-github-action-ci-cd-demo/blob/main/.github/workflows/CD-Job.yml
return {"ok": true, "message": `Packaging request sent successfully!<br/>Branch: <strong>${object.branch}</strong><br/>Build number: <strong>${object.buildNumber}</strong>`};
} catch (error) {
return {"ok": false, "message": "An error occurred: "+error.message};
}
}
The processForm
method handles the form submission content and can also include more validations.
GAS x Github API x Github Action
Here is some additional information related to the corresponding Github Action.
CD-Job.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# CD Packaging Job
name: CD-Job
on:
workflow_dispatch:
inputs:
buildNumber: # Corresponds to GAS payload.inputs.xxx
description: 'Version number'
required: false
type: string
# ...More
# For input types, refer to the official documentation: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputs
jobs:
some-job:
runs-on: ubuntu-latest
steps:
- name: Print Inputs
run: \|
echo "Release Build Number: ${{ github.event.inputs.buildNumber }}"
Step 4. Redeploy Google Apps Script Web App Form
⚠️Please note, any changes to the GAS code require redeployment to take effect.⚠️
⚠️Please note, any changes to the GAS code require redeployment to take effect.⚠️
⚠️Please note, any changes to the GAS code require redeployment to take effect.⚠️
In the top right corner of GAS, select “Deploy” -> choose “Edit” in the top right corner -> under Version, select “Create new version”
Click “Deploy” -> Done.
Go back to the webpage and refresh to see the updated results:
⚠️Please note, any changes to the GAS code require redeployment to take effect.⚠️
⚠️ Please note that any changes to the GAS code require redeployment to take effect. ⚠️
⚠️ Please note that any changes to the GAS code require redeployment to take effect. ⚠️
Done! 🎉🎉🎉
You can now share this link within your organization, allowing others to directly use this web GUI to perform CI/CD tasks.
Extension (1) — Query Slack User ID by User Email & Send, Update Progress Notifications
As mentioned earlier, we want to promptly notify users about the CI/CD execution status. We can use the email provided by the user to look up their Slack User ID.
Slack.gs:
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
const slackBotToken = ""
// https://medium.com/zrealm-robotic-process-automation/slack-chatgpt-integration-bd94cc88f9c9
function slackRequest(path, content) {
const options = {
method: "post",
contentType: "application/json",
headers: {
Authorization: `Bearer ${slackBotToken}`, // Use the bot token for authorization,
'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;
}
}
// Query Slack UID by email
function getSlackUserId(email) {
return slackRequest(`users.lookupByEmail?email=${encodeURIComponent(email)}`)?.user?.id;
}
// Send message to target Slack UID (channelID)
function sendSlackMessage(channelId, ts = null, value) {
var content = {
channel: channelId
};
if (ts != null) {
content.thread_ts = ts;
}
if (typeof value === "string") {
content.text = value;
} else {
content.blocks = value;
}
return slackRequest("chat.postMessage", content);
}
// Update sent message content
function updateSlackMessage(channelId, ts = null, value) {
var content = {
channel: channelId
};
if (ts != null) {
content.ts = ts;
}
if (typeof value === "string") {
content.text = value;
} else {
content.blocks = value;
}
return slackRequest("chat.update", content);
}
For using the Slack API, please refer to the official documentation.
GitHub Action YAML can use this Action to continuously update messages and send Slack 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
# ...
on:
workflow_dispatch:
inputs:
buildNumber: # Corresponds to GAS payload.inputs.xxx
description: 'Version number'
required: false
type: string
# ...More
SLACK_USER_ID:
description: 'Slack User Id for receiving action notification'
type: string
SLACK_CHANNEL_ID:
description: 'Slack Channel Id for receiving action notification'
type: string
SLACK_THREAD_TS:
description: 'Slack message ts'
type: string
jobs:
# some jobs...
if-deploy-failed-message:
runs-on: ubuntu-latest
if: failure()
- name: update slack message
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \|
channel: ${{ github.event.inputs.SLACK_CHANNEL_ID }}
ts: ${{ github.event.inputs.SLACK_THREAD_TS }}
text: "❌ Packaging task failed, please check the execution status or try again later.\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\|Click here to view the status> cc'ed <@${{ github.event.inputs.SLACK_USER_ID }}>"
Effect:
For details on Slack App integration, please refer to my previous article: Slack & ChatGPT Integration.
Extension (2) — Query Jira Tickets
Jira.gs:
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
const jiraPersonalAccessToken = ""
// https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html
function getJiraTickets() {
const url = `https://xxx.atlassian.net/rest/api/3/search`;
// JQL query
const jql = `project = XXX`;
const queryParams = {
jql: jql,
maxResults: 50, // Adjust as needed
};
const options = {
method: "get",
headers: {
Authorization: "Basic " + jiraPersonalAccessToken,
"Content-Type": "application/json",
},
muteHttpExceptions: true,
};
const queryString = Object.keys(queryParams).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
const response = UrlFetchApp.fetch(url + "?" + queryString + "&fields=", options);
// could specify only return some fields
if (response.getResponseCode() === 200) {
const issues = JSON.parse(response.getContentText()).issues;
return issues;
} else {
Logger.log(`Error: ${response.getResponseCode()} - ${response.getContentText()}`);
throw new Error("Failed to fetch Jira issues.");
}
}
For other Jira API usage, please refer to the official documentation.
Extension (3) — Query Asana Tasks
Asana.gs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const asanaPersonalAccessToken = ""
// https://developers.asana.com/docs/personal-access-token
function asanaAPI(endpoint, method = "GET", data = null) {
var options = {
"method" : method,
"contentType" : "application/json",
"headers": {
"Authorization": "Bearer "+asanaPersonalAccessToken
}
};
if (data != null) {
options["payload"] = JSON.stringify({"data":data});
}
const url = "https://app.asana.com/api/1.0"+endpoint;
const res = UrlFetchApp.fetch(url, options);
const data = JSON.parse(res.getContentText());
return data;
}
// Find tasks in project
// asanaAPI("/projects/PROJECT_ID/tasks");
For other Asana API uses, please refer to the official documentation.
Summary
Automation, work, and development process optimization never lack technology, but ideas; as long as we have ideas, we can find the right technology to realize them. Let’s encourage each other!
2025/07 Update:
This feature has been integrated into the actual packaging tool. Please refer to the latest article example: “CI/CD Practical Guide (Part 4): Using Google Apps Script Web App to Connect GitHub Actions to Build a Free and Easy Packaging Tool Platform”
If you have any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.