Using Google Apps Script Web App Forms to Integrate with GitHub Action CI/CD Workflows
Optimizing GitHub Action Workflow forms and integrating with other workflow tools (Jira, Asana, Slack...) to enhance development efficiency.
ℹ️ℹ️ℹ️ The following content is translated by OpenAI.
Click here to view the original Chinese version. | 點此查看本文中文版
Using Google Apps Script Web App Forms to Integrate with GitHub Action CI/CD Workflows
Optimizing GitHub Action Workflow forms and integrating with other workflow tools (Jira, Asana, Slack…) to enhance development efficiency.
Left: Original GitHub Action Workflow Form / Right: Final Result (GAS Web App Form)
Background
The previous team used GitHub Action & Self-hosted GitHub Runner + Slack to set up a complete CI/CD service; the overall effect was good. For app developers, building and maintaining it was relatively easy. They just needed to follow the YAML parameters provided in the official documentation to complete the setup, which would automatically trigger. On the machine side, they could easily use their own machines as runners. The service itself is maintained by GitHub, so we don’t have to worry about version upgrades and other issues, and the runner pulls tasks from GitHub, so there’s no need to open external network ports. The best part for the team is that running GitHub Action on their own runner is completely free.
This means we can enjoy a GUI YAML build method similar to Bitrise 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.
I will write a complete article on the App CI/CD x GitHub Action setup process when I have time in the future.
Problem: GitHub Action CI/CD GUI Form
GitHub Action GUI Form
In app development, when triggering CD to package a test version, production version, or for review, it usually requires providing some external parameters or selecting environments and branches according to needs before starting the workflow.
Unlike Jenkins, which is a self-hosted service with a complete web GUI, GitHub Action does not have one. The only web GUI form is a simple form that users can fill out by clicking “Run workflow” in Actions to input external parameters and trigger the CI/CD workflow.
Typically, users who package using this CD may not be the app developers themselves and may not have permissions for the project. For example, QA may need to package a specific version, or PM/Backend may need to package a certain development version for testing. The GitHub Action Form requires permissions for the project to use, but users may not have project permissions or even an engineering background.
Moreover, we cannot create dynamic forms or data validation here.
Therefore, we need to create another GUI service for other users to operate.
Self-built Slack App Solution
Previously, a team member who loved automation built a complete Slack App web service using Kotlin + Ktor, integrating Slack messages, forms, commands, etc., to receive and forward CD packaging requests, trigger GitHub Action operations, and send results back to Slack.
Currently, we do not have development resources to build the service using Kotlin + Ktor as before.
Custom Web/iOS/macOS App Tool
The team originally used Jenkins, which had a basic web interface for other users to log in, and also developed an app that integrated with Jenkins to package some parameters for non-engineering users to use more conveniently.
However, after migrating to GitHub Action, this entire setup became obsolete.
❌ Private GitHub Pages
There should be a chance to directly build GitHub Pages as a CI/CD web GUI, but currently, only GitHub Enterprise can set GitHub Pages access permissions. Other plans, even if they are private repos, will be public; there is no security to speak of.
❌ Slack App, but using Google Apps Script for Construction
Initially, I thought about using a Slack App as a CI/CD GUI Form service based on the previous team’s experience, but currently, we do not have resources to build the service using Kotlin + Ktor as before. So I thought about quickly trying to build it using Function as a Service.
There are many types of Function as a Service, and Cloud Functions offers more flexibility. However, due to organizational IT restrictions, we cannot freely add Public Cloud Functions, and there are cost issues; thus, we return to our old friend — Google Apps Script.
I have previously written several articles about automating with Google Apps Script; interested friends can refer to:
1. “Automating Daily Data Reports with Google Apps Script RPA“
2. “Simple 3 Steps — Create a Free GA4 Automatic Data Notification Bot“
3. “Crashlytics + Google Analytics Automatic Query for App Crash-Free Users Rate“
4. “Crashlytics + Big Query Creating a More Immediate and Convenient Crash Tracking Tool“
In summary, Google Apps Script is another Function as a Service provided by Google, with the main features being free and quick integration with Google services. However, there are also many limitations, such as only being able to use its language, execution time cannot exceed 6 minutes, there are limits on execution counts, and it does not support multithreading, etc. For details, you can refer to my previous article.
The conclusion is that it is not feasible due to:
- Function as a Service cold start issues. If the service is not called for a period, it goes to sleep, and calling it again takes longer to start (3~≥ 5 seconds); Slack App has very strict API response time requirements, and the service needs to respond within 3 seconds; otherwise, it is considered a failure, leading to errors from Slack and event listeners being considered lost, resulting in duplicate sends.
- Google Apps Script doGet, doPost methods cannot retrieve headers. This prevents us from using official security verification and from disabling Slack Retry.
- Google Apps Script single-threaded issues. If we need to connect to other services, the response time will exceed 3 seconds, which Slack will directly determine as a failure.
I managed to use Slack messages, Block Kit, and Forms to connect the entire process, but it was too easy to trigger the above issues, so I eventually gave up.
If you want to do this, you still need to set up your own server and service; do not use Function as a Service!
❌ Slack Workflow Form
Slack Workflow Form (❌ Cannot be customized)
I also tried Slack’s built-in automation feature, the Workflow Form, but it cannot create dynamic form content (e.g., fetching branches for user selection); the only customizable part is the subsequent data submission step.
✅ Google Apps Script Web App GUI Form
When one path is blocked, another opens. It seems we don’t have to be fixated on integrating with Slack. While integrating with Slack is the best solution since it directly integrates into existing team collaboration tools without needing to learn new tools, due to resource constraints, we can only settle for finding other stable and user-friendly methods.
Looking back, Google Apps Script can be deployed as a Web App, allowing us to respond with a GUI Form when handling Web doGet requests, and after submitting the form, it can trigger subsequent GitHub integration processing.
Final Result 🎉
Workflow
We built a CI/CD form using Google Apps Script Web App, directly binding it to Google Workspace accounts and setting it so that only users within the organization can access it. It automatically retrieves the currently logged-in user’s email, uses a shared GitHub repo account (or borrows a personal access token from an account with permissions) to call the GitHub API to get the branch list, and after submission, it also calls the API to trigger GitHub Action to start executing the CI/CD workflow.
Additionally, we can use the user’s email to call the Slack API through the Slack App to obtain that user’s Slack ID, and then send messages through the Slack App to notify the status of the CI/CD task execution.
We can also integrate with other tools and development processes, such as first retrieving tickets from Asana or Jira, selecting them, and then using the GitHub API to find branches, trigger GitHub Action, and finally notify the user via 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 Program
Having not written HTML or CSS for a long time and being too lazy to design styles myself, I directly asked ChatGPT to generate an HTML form template with some design.
In the left file list of GAS, click the “+” to add a file, name it “Form.html
”, and paste the HTML form template content generated by GPT.
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">Version Number:</label>
<input type="number" value="<?=buildNumber?>"/>
</div>
<div class="form-group">
<label for="branch">Reviewing PR:</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 Content:</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>
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
The content of the form can be adjusted according to your needs.
`code.gs:`
```javascript
function doGet(e) {
// Corresponds to the left file Form.html
const htmlTemplate = HtmlService.createTemplateFromFile('Form');
const email = Session.getActiveUser().getEmail();
// Get the user's email, limited to the execution identity: users accessing the web application
const title = "App CD Packaging Request Form";
const buildNumber = genBuildNumber();
htmlTemplate.email = email;
htmlTemplate.title = title;
htmlTemplate.pullRequests = []; // Next step is to connect to Github...
htmlTemplate.buildNumber = buildNumber;
const html = htmlTemplate.evaluate();
html.setTitle(title);
//html.setWidth(600) // Set the 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, and in the next step, we will connect to the Github API to retrieve the PR branch list.
Step 2. Deploy Google Apps Script Web App Form
Let’s deploy the content we just created to check the results.
In the top right corner of GAS, select “Deploy” -> “New Deployment” -> “Web App”:
The execution identity and who can access can be set as follows:
Execution Identity: Me The script will run under your account.
- Who can access: Only myself
- Who can access: All users in the same organization
Only users from the same organization with logged-in Google accounts can access.
- Who can access: All logged-in Google account users
All logged-in Google account users can access.
- Who can access: Everyone
No need to log in to a Google account; everyone can access publicly.
Execution Identity: Users accessing the web application The script will run under your account.
- Who can access: Only myself
- Who can access: All users in the same organization
Only users from the same organization with logged-in Google accounts can access.
- Who can access: All logged-in Google account users
All logged-in Google account users can access.
By selecting “Who can access: All users in the same organization” + “Execution Identity: Users accessing the web application,” we can automatically restrict access to only organization accounts and run it under their own identity!
This is a very convenient permission control feature!
After making your selections, click “Deploy” in the bottom right.
The URL in the web application is the access URL for the Web App.
1
https://script.google.com/macros/s/AKfycbw8SuK7lLLMdY86y3jxMJyzXqa5tdxJryRnteOnNi-lK--j6CmKYXj7UuU58DiS0NSVvA/exec
The URL is long and ugly, but there’s nothing we can do about it; we can only find a URL shortening tool to shorten it.
Click the URL to open the page and see the effect:
Here are two additional GAS limitations to note:
- The warning message at the top of the GAS Web App cannot be hidden by default.
- The GAS Web App embeds our page using an IFrame, making it difficult to achieve 100% responsive design (RWD) effects. You can only use
.setWidth()
to adjust the window width.
Google Apps Script Authorization Warning
The first time you use it, clicking “Debug” or “Run” may trigger the following authorization warning:
Select the account you want to execute with. If you see “This application is not verified by Google,” click “Advanced” -> “Go to XXX (unsafe)” and select “Allow”:
If the permissions for the GAS program change (for example, adding access to Google Sheets, etc.), you will need to reauthorize; otherwise, it will not appear again after being approved once.
Step 3. Connect to the Github API to Retrieve PR Branch List
We will add a new Github.gs
script 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 from 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 finer query functions of the Github API are only available through the 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);
There are two ways to access the Github API: the traditional Restful method and the more flexible GraphQL method; this article uses Restful as an example.
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
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 left file Form.html
const htmlTemplate = HtmlService.createTemplateFromFile('Form');
const email = Session.getActiveUser().getEmail();
// Get the user's email, limited to the execution identity: users accessing the web application
const title = "App CD Packaging Request Form";
const pullRequests = githubAPI("get", "/repos/swiftlang/swift/pulls");
// Using https://github.com/swiftlang/swift/pulls as an example
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 the page width
return html
}
function processForm(object) {
if (object.buildNumber == "") {
return {"ok": false, "message": "Please enter a version number!"};
}
if (object.branch == "") {
return {"ok": false, "message": "Please select a branch version!"};
}
// Include the parameters you want to pass 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);
// Using https://github.com/zhgchgli0718/ios-project-for-github-action-ci-cd-demo/blob/main/.github/workflows/CD-Job.yml as an example
return {"ok": true, "message": `Packaging request sent successfully!<br/>Corresponding branch: <strong>${object.branch}</strong><br/>Version number: <strong>${object.buildNumber}</strong>`};
} catch (error) {
return {"ok": false, "message": "An error occurred: " + error.message};
}
}
The processForm
method handles the content returned from the form and can also include more parameters.
GAS x Github API x Github Action
Here, I will add some details corresponding to the 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
# Input types can 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 that any adjustments to the GAS code require redeployment to take effect. ⚠️
⚠️ Please note that any adjustments to the GAS code require redeployment to take effect. ⚠️
⚠️ Please note that any adjustments to the GAS code require redeployment to take effect. ⚠️
In the top right corner of GAS, select “Deploy” -> choose “Edit” in the top right -> select “Create new version”.
Click “Deploy” -> complete.
Then return to the webpage and refresh to see the modified results:
⚠️ Please note that any adjustments to the GAS code require redeployment to take effect. ⚠️
⚠️ Please note that any adjustments to the GAS code require redeployment to take effect. ⚠️
⚠️ Please note that any adjustments to the GAS code require redeployment to take effect. ⚠️
Done! 🎉🎉🎉
Now you can share this link within your organization with other partners, and they can directly use this web GUI to execute CI/CD tasks.
Extension (1) — Query Slack User ID Using User’s Email & Send/Update Progress Notifications
As mentioned earlier, we want to notify users of the CI/CD execution status in real-time. We can use the email provided by the user to look up the 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 using email
function getSlackUserId(email) {
return slackRequest(`users.lookupByEmail?email=${encodeURIComponent(email)}`)?.user?.id;
}
// Send a message to the 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 the content of the sent message
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 Slack API usage, 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 notifications'
type: string
SLACK_CHANNEL_ID:
description: 'Slack Channel Id for receiving action notifications'
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 execution status> cc'ed <@${{ github.event.inputs.SLACK_USER_ID }}>"
Effect:
For details on integrating Slack Apps, 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
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,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
```javascript
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 usage, please refer to the official documentation.
Summary
The missing element in automation, work, and development process optimization is never technology, but rather ideas; as long as we have ideas, we can find the right technology to implement them. Let’s encourage each other!
If you have any questions or suggestions, feel free to contact me.
```
This article was first published on Medium ➡️ Click Here
Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.