Skip to content

Commit 9ebedd9

Browse files
authored
feat: add adaptive card template (#17)
1 parent 3f5ae20 commit 9ebedd9

File tree

6 files changed

+322
-103
lines changed

6 files changed

+322
-103
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@typescript-eslint/ban-ts-comment": "error",
2020
"camelcase": "off",
2121
"@typescript-eslint/consistent-type-assertions": "error",
22-
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
22+
"@typescript-eslint/explicit-function-return-type": 0,
2323
"@typescript-eslint/func-call-spacing": ["error", "never"],
2424
"@typescript-eslint/no-array-constructor": "error",
2525
"@typescript-eslint/no-empty-interface": "error",

ac.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"type": "AdaptiveCard",
3+
"body": [
4+
{
5+
"type": "TextBlock",
6+
"size": "large",
7+
"weight": "bolder",
8+
"text": "Workflow '${$root.workflow.name}' #${$root.workflow.run_number} ${$root.workflow.conclusion}",
9+
"color": "${$root.workflow.conclusion_color}",
10+
"fontType": "Default",
11+
"separator": true
12+
},
13+
{
14+
"type": "TextBlock",
15+
"text": "on [${$root.repository.name}](${$root.repository.html_url})",
16+
"wrap": true,
17+
"spacing": "None"
18+
},
19+
{
20+
"type": "ColumnSet",
21+
"columns": [
22+
{
23+
"type": "Column",
24+
"items": [
25+
{
26+
"type": "Image",
27+
"style": "Person",
28+
"url": "${$root.author.avatar_url}",
29+
"size": "Medium"
30+
}
31+
],
32+
"width": "auto"
33+
},
34+
{
35+
"type": "Column",
36+
"items": [
37+
{
38+
"type": "TextBlock",
39+
"weight": "Bolder",
40+
"text": "[${$root.author.username}](${$root.author.html_url})",
41+
"wrap": true
42+
}
43+
],
44+
"width": "stretch"
45+
}
46+
],
47+
"spacing": "Medium"
48+
},
49+
{
50+
"type": "FactSet",
51+
"facts": [
52+
{
53+
"title": "Commit",
54+
"value": "[${$root.commit.message}](${$root.commit.html_url})"
55+
},
56+
{
57+
"title": "${$root.event.type}",
58+
"value": "[${$root.event.html_url}](${$root.event.html_url})"
59+
},
60+
{
61+
"title": "Workflow run details",
62+
"value": "[${$root.workflow.run_html_url}](${$root.workflow.run_html_url})"
63+
}
64+
],
65+
"height": "stretch",
66+
"separator": true,
67+
"spacing": "Medium"
68+
}
69+
],
70+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
71+
"version": "1.2"
72+
}

dist/main/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opsless/ms-teams-github-actions",
3-
"version": "0.1.6",
3+
"version": "0.2.0",
44
"private": true,
55
"description": "MS Teams Github Actions integration",
66
"main": "lib/main.js",
@@ -18,7 +18,10 @@
1818
"dependencies": {
1919
"@actions/core": "^1.2.6",
2020
"@actions/github": "^4.0.0",
21-
"axios": "^0.21.1"
21+
"adaptive-expressions": "^4.12.0",
22+
"adaptivecards-templating": "^2.1.0",
23+
"axios": "^0.21.1",
24+
"cockatiel": "^2.0.1"
2225
},
2326
"devDependencies": {
2427
"@types/jest": "^26.0.7",

src/main.ts

Lines changed: 107 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as core from '@actions/core'
22
import * as github from '@actions/github'
33
import * as axios from 'axios'
4+
import * as fs from 'fs'
5+
import {Template} from 'adaptivecards-templating'
46

57
async function sleep(ms: number): Promise<unknown> {
68
return new Promise(resolve => {
@@ -24,107 +26,114 @@ enum StepStatus {
2426
COMPLETED = 'completed'
2527
}
2628

27-
async function run(): Promise<unknown> {
28-
try {
29-
const token = core.getInput('github-token')
30-
if (!token) {
31-
core.setFailed("'github-token' input can't be empty")
32-
return
33-
}
29+
enum TextBlockColor {
30+
Good = 'good',
31+
Attention = 'attention',
32+
Warning = 'warning'
33+
}
3434

35-
const webhookUri = core.getInput('webhook-uri')
36-
if (!webhookUri) {
37-
core.setFailed("'webhook-uri' input can't be empty")
38-
return
39-
}
40-
const ctx = github.context
41-
const o = github.getOctokit(token)
42-
43-
core.debug(JSON.stringify(ctx))
44-
45-
await sleep(5000)
46-
47-
const jobList = await o.actions.listJobsForWorkflowRun({
48-
repo: ctx.repo.repo,
49-
owner: ctx.repo.owner,
50-
run_id: ctx.runId
51-
})
52-
53-
const jobs = jobList.data.jobs
54-
core.debug(JSON.stringify(jobs))
55-
56-
const job = jobs.find(j => j.name.startsWith(ctx.job))
57-
58-
const stoppedStep = job?.steps.find(
59-
s =>
60-
s.conclusion === Conclusions.FAILURE ||
61-
s.conclusion === Conclusions.TIMED_OUT ||
62-
s.conclusion === Conclusions.TIMED_OUT ||
63-
s.conclusion === Conclusions.ACTION_REQUIRED
64-
)
65-
const lastStep = stoppedStep
66-
? stoppedStep
67-
: job?.steps.reverse().find(s => s.status === StepStatus.COMPLETED)
68-
69-
const wr = await o.actions.getWorkflowRun({
70-
owner: ctx.repo.owner,
71-
repo: ctx.repo.repo,
72-
run_id: ctx.runId
73-
})
74-
75-
core.debug(JSON.stringify(wr.data))
76-
77-
const repository_url = ctx.payload.repository?.html_url
78-
const commit_author = ctx.actor
79-
80-
const themeColor =
81-
lastStep?.conclusion === Conclusions.SUCCESS
82-
? '90C978'
83-
: lastStep?.conclusion === Conclusions.CANCELLED
84-
? 'FFF175'
85-
: 'C23B23'
86-
const conclusion =
87-
lastStep?.conclusion === Conclusions.SUCCESS
88-
? 'SUCCEEDED'
89-
: lastStep?.conclusion === Conclusions.CANCELLED
90-
? 'CANCELLED'
91-
: 'FAILED'
92-
93-
const webhookBody = {
94-
'@type': 'MessageCard',
95-
'@context': 'http://schema.org/extensions',
96-
themeColor: `${themeColor}`,
97-
summary: `${commit_author} commited new changes`,
98-
sections: [
99-
{
100-
activityTitle: `Workflow '${ctx.workflow}' #${ctx.runNumber} ${conclusion}`,
101-
activitySubtitle: `on [${ctx.payload.repository?.full_name}](${repository_url})`,
102-
facts: [
103-
{
104-
name: 'Commit',
105-
value: `[${wr.data.head_commit.message}](${wr.data.repository.html_url}/commit/${wr.data.head_sha}) by [${ctx.payload.sender?.login}](${ctx.payload.sender?.html_url})`
106-
},
107-
{
108-
name:
109-
ctx.eventName === 'pull_request' ? 'Pull request' : 'Branch',
110-
value:
111-
ctx.eventName === 'pull_request'
112-
? `[${ctx.payload.pull_request?.html_url}](${ctx.payload.pull_request?.html_url})`
113-
: `[${ctx.payload.repository?.html_url}/tree/${ctx.ref}](${ctx.payload.repository?.html_url}/tree/${ctx.ref})`
114-
},
115-
{
116-
name: 'Workflow run details',
117-
value: `[${wr.data.html_url}](${wr.data.html_url})`
118-
}
119-
],
120-
markdown: true
121-
}
122-
]
35+
const send = async () => {
36+
await sleep(5000)
37+
const token = core.getInput('github-token')
38+
const webhookUri = core.getInput('webhook-uri')
39+
const o = github.getOctokit(token)
40+
const ctx = github.context
41+
const jobList = await o.actions.listJobsForWorkflowRun({
42+
repo: ctx.repo.repo,
43+
owner: ctx.repo.owner,
44+
run_id: ctx.runId
45+
})
46+
47+
const jobs = jobList.data.jobs
48+
49+
const job = jobs.find(j => j.name.startsWith(ctx.job))
50+
51+
const stoppedStep = job?.steps.find(
52+
s =>
53+
s.conclusion === Conclusions.FAILURE ||
54+
s.conclusion === Conclusions.TIMED_OUT ||
55+
s.conclusion === Conclusions.TIMED_OUT ||
56+
s.conclusion === Conclusions.ACTION_REQUIRED
57+
)
58+
const lastStep = stoppedStep
59+
? stoppedStep
60+
: job?.steps.reverse().find(s => s.status === StepStatus.COMPLETED)
61+
62+
const wr = await o.actions.getWorkflowRun({
63+
owner: ctx.repo.owner,
64+
repo: ctx.repo.repo,
65+
run_id: ctx.runId
66+
})
67+
68+
const conclusion =
69+
lastStep?.conclusion === Conclusions.SUCCESS
70+
? 'SUCCEEDED'
71+
: lastStep?.conclusion === Conclusions.CANCELLED
72+
? 'CANCELLED'
73+
: 'FAILED'
74+
75+
const conclusion_color =
76+
lastStep?.conclusion === Conclusions.SUCCESS
77+
? TextBlockColor.Good
78+
: lastStep?.conclusion === Conclusions.CANCELLED
79+
? TextBlockColor.Warning
80+
: TextBlockColor.Attention
81+
82+
const rawdata = fs.readFileSync('./ac.json').toString()
83+
const template = new Template(rawdata)
84+
const content = template.expand({
85+
$root: {
86+
repository: {
87+
name: ctx.payload.repository?.full_name,
88+
html_url: ctx.payload.repository?.html_url
89+
},
90+
commit: {
91+
message: wr.data.head_commit.message,
92+
html_url: `${wr.data.repository.html_url}/commit/${wr.data.head_sha}`
93+
},
94+
workflow: {
95+
name: ctx.workflow,
96+
conclusion,
97+
conclusion_color,
98+
run_number: ctx.runNumber,
99+
run_html_url: wr.data.html_url
100+
},
101+
event: {
102+
type: ctx.eventName === 'pull_request' ? 'Pull request' : 'Branch',
103+
html_url:
104+
ctx.eventName === 'pull_request'
105+
? ctx.payload.pull_request?.html_url
106+
: `${ctx.payload.repository?.html_url}/tree/${ctx.ref}`
107+
},
108+
author: {
109+
username: ctx.payload.sender?.login,
110+
html_url: ctx.payload.sender?.html_url,
111+
avatar_url: ctx.payload.sender?.avatar_url
112+
}
123113
}
124-
const response = await axios.default.post(webhookUri, webhookBody)
125-
core.debug(JSON.stringify(response.data))
126-
// TODO: check response status, if not succesful, mark workflow as failed
114+
})
115+
116+
const webhookBody = {
117+
type: 'message',
118+
attachments: [
119+
{
120+
contentType: 'application/vnd.microsoft.card.adaptive',
121+
content: JSON.parse(content)
122+
}
123+
]
124+
}
125+
126+
core.info(JSON.stringify(webhookBody))
127+
128+
const response = await axios.default.post(webhookUri, webhookBody)
129+
core.info(JSON.stringify(response.data))
130+
}
131+
132+
async function run() {
133+
try {
134+
await send()
127135
} catch (error) {
136+
core.error(error)
128137
core.setFailed(error.message)
129138
}
130139
}

0 commit comments

Comments
 (0)