Skip to content

Commit e4f2c13

Browse files
authored
Merge pull request #57 from grid-x/feat/make-body-configurable
feat: Make post body template configurable
2 parents ba0f0db + e02d4bf commit e4f2c13

File tree

10 files changed

+292
-33
lines changed

10 files changed

+292
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,4 @@ __tests__/runner/*
101101
.idea
102102
.vscode
103103
*.code-workspace
104+
.claude

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
22
1+
24

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ and update a given topic with the newly uploaded file.
2525
`git rev-parse --short HEAD`
2626
- `spec_file` - the specification file to be uploaded, relative to the
2727
repositories root
28+
- `body_template` _(optional)_ - custom post body template. Supports
29+
placeholders: `{ORIGINAL_FILENAME}`, `{DISCOURSE_URL}`, `{UPLOAD_PATH}`,
30+
`{UPLOAD_URL}`, `{DATE}`, `{COMMIT}`. Defaults to:
31+
32+
````
33+
API Documentation/Specification `{ORIGINAL_FILENAME}`
34+
35+
```apidoc
36+
https://{DISCOURSE_URL}/{UPLOAD_PATH}
37+
```
38+
39+
[{ORIGINAL_FILENAME}|attachment]({UPLOAD_URL})
40+
41+
*last updated*: {DATE} (sha {COMMIT})
42+
````
2843

2944
## Instructions
3045

__tests__/main.test.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import * as core from '@actions/core'
2+
import Axios from 'axios'
3+
import fs from 'fs'
4+
import { postBody, run } from '../src/main'
5+
6+
jest.mock('@actions/core')
7+
jest.mock('axios')
8+
jest.mock('fs', () => ({
9+
...jest.requireActual('fs'),
10+
readFileSync: jest.fn()
11+
}))
12+
jest.mock('form-data', () =>
13+
jest.fn().mockImplementation(() => ({
14+
append: jest.fn(),
15+
getHeaders: jest.fn().mockReturnValue({})
16+
}))
17+
)
18+
19+
const uploadResult = {
20+
url: 'https://example.com/uploads/file.yaml',
21+
short_url: 'upload://abc123.yaml',
22+
short_path: 'uploads/short/abc123.yaml',
23+
original_filename: 'petstore.yaml'
24+
}
25+
26+
describe('postBody', () => {
27+
it('generates a post body with the expected content', () => {
28+
const fixedDate = '2024-01-01T00:00:00.000Z'
29+
jest
30+
.spyOn(global, 'Date')
31+
.mockImplementation(() => ({ toISOString: () => fixedDate }) as any)
32+
33+
const result = postBody('discourse.example.com', uploadResult, 'abc1234 ')
34+
35+
expect(result).toContain('petstore.yaml')
36+
expect(result).toContain(
37+
'https://discourse.example.com/uploads/short/abc123.yaml'
38+
)
39+
expect(result).toContain('upload://abc123.yaml')
40+
expect(result).toContain('sha abc1234') // commit is trimmed
41+
expect(result).toContain(fixedDate)
42+
43+
jest.restoreAllMocks()
44+
})
45+
46+
it('matches the exact full content', () => {
47+
const fixedDate = '2024-01-01T00:00:00.000Z'
48+
jest
49+
.spyOn(global, 'Date')
50+
.mockImplementation(() => ({ toISOString: () => fixedDate }) as any)
51+
52+
const result = postBody('discourse.example.com', uploadResult, 'abc1234 ')
53+
54+
expect(result).toBe(
55+
'API Documentation/Specification `petstore.yaml`\n' +
56+
'\n' +
57+
'```apidoc\n' +
58+
'https://discourse.example.com/uploads/short/abc123.yaml\n' +
59+
'```\n' +
60+
'\n' +
61+
'[petstore.yaml|attachment](upload://abc123.yaml)\n' +
62+
'\n' +
63+
'*last updated*: 2024-01-01T00:00:00.000Z (sha abc1234)\n'
64+
)
65+
66+
jest.restoreAllMocks()
67+
})
68+
69+
it('uses a custom body template when provided', () => {
70+
const customTemplate = 'Custom: {ORIGINAL_FILENAME} at {DISCOURSE_URL}'
71+
const result = postBody(
72+
'discourse.example.com',
73+
uploadResult,
74+
'abc1234',
75+
customTemplate
76+
)
77+
expect(result).toBe('Custom: petstore.yaml at discourse.example.com')
78+
})
79+
})
80+
81+
const mockPost = jest.fn()
82+
const mockHttp = {
83+
post: mockPost,
84+
interceptors: { request: { use: jest.fn() } }
85+
}
86+
87+
const uploadApiResponse = {
88+
url: 'https://discourse.example.com/uploads/file.yaml',
89+
short_url: 'upload://abc123.yaml',
90+
short_path: 'uploads/short/abc123.yaml',
91+
original_filename: 'petstore.yaml'
92+
}
93+
94+
const runArgs = [
95+
'discourse.example.com',
96+
'42',
97+
'api-key',
98+
'api-user',
99+
'abc1234',
100+
'petstore.yaml'
101+
] as const
102+
103+
describe('run', () => {
104+
beforeEach(() => {
105+
;(Axios.create as jest.Mock).mockReturnValue(mockHttp)
106+
;(fs.readFileSync as jest.Mock).mockReturnValue(Buffer.from('spec content'))
107+
})
108+
109+
describe('success', () => {
110+
beforeEach(() => {
111+
mockPost.mockResolvedValue({ data: uploadApiResponse })
112+
global.fetch = jest.fn().mockResolvedValue({ statusText: 'OK' })
113+
})
114+
115+
it('creates the Axios instance with the correct base URL and auth headers', async () => {
116+
await run(...runArgs)
117+
118+
expect(Axios.create).toHaveBeenCalledWith(
119+
expect.objectContaining({
120+
baseURL: 'https://discourse.example.com',
121+
headers: expect.objectContaining({
122+
'Api-Key': 'api-key',
123+
'Api-Username': 'api-user'
124+
})
125+
})
126+
)
127+
})
128+
129+
it('reads the spec file and posts it to /uploads.json', async () => {
130+
await run(...runArgs)
131+
132+
expect(fs.readFileSync).toHaveBeenCalledWith('petstore.yaml')
133+
expect(mockPost).toHaveBeenCalledWith(
134+
'/uploads.json',
135+
expect.anything(),
136+
{
137+
params: { type: 'composer', synchronous: true }
138+
}
139+
)
140+
})
141+
142+
it('sends a PUT request to the post URL with auth headers and post body', async () => {
143+
await run(...runArgs)
144+
145+
expect(global.fetch).toHaveBeenCalledWith(
146+
'https://discourse.example.com/posts/42.json',
147+
expect.objectContaining({
148+
method: 'PUT',
149+
headers: expect.objectContaining({
150+
'Content-Type': 'application/json',
151+
'Api-Key': 'api-key',
152+
'Api-Username': 'api-user'
153+
})
154+
})
155+
)
156+
const body = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body)
157+
expect(body.post.edit_reason).toBe('Uploaded spec at abc1234')
158+
expect(body.post.raw).toContain('petstore.yaml')
159+
})
160+
})
161+
162+
it('calls core.setFailed when the upload request fails', async () => {
163+
mockPost.mockRejectedValue(new Error('Network error'))
164+
jest.spyOn(console, 'error').mockImplementation(() => {})
165+
166+
await run(...runArgs)
167+
168+
expect(core.setFailed).toHaveBeenCalledWith('Network error')
169+
})
170+
171+
it('calls core.error and process.exit(1) when updating the post fails', async () => {
172+
mockPost.mockResolvedValue({ data: uploadApiResponse })
173+
global.fetch = jest.fn().mockRejectedValue(new Error('Fetch error'))
174+
const mockExit = jest
175+
.spyOn(process, 'exit')
176+
.mockImplementation(() => undefined as never)
177+
178+
await run(...runArgs)
179+
180+
expect(core.error).toHaveBeenCalledWith(new Error('Fetch error'))
181+
expect(mockExit).toHaveBeenCalledWith(1)
182+
mockExit.mockRestore()
183+
})
184+
})

action.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ inputs:
3131
github_sha:
3232
description: 'short commit hash to be put into to post for traceability'
3333
required: true
34+
body_template:
35+
description: |
36+
Custom post body template. Supports placeholders:
37+
{ORIGINAL_FILENAME}, {DISCOURSE_URL}, {UPLOAD_PATH}, {UPLOAD_URL}, {DATE}, {COMMIT}
38+
required: false
39+
default: |
40+
API Documentation/Specification `{ORIGINAL_FILENAME}`
41+
42+
```apidoc
43+
https://{DISCOURSE_URL}/{UPLOAD_PATH}
44+
```
45+
46+
[{ORIGINAL_FILENAME}|attachment]({UPLOAD_URL})
47+
48+
*last updated*: {DATE} (sha {COMMIT})
3449
3550
runs:
3651
using: node20

dist/index.js

Lines changed: 28 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

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

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ const discourseUser: string = core.getInput('discourse_user')
1212
const commit: string = core.getInput('github_sha')
1313

1414
const specFile: string = core.getInput('spec_file')
15+
const bodyTemplate: string = core.getInput('body_template')
1516
run(
1617
discourseUrl,
1718
discoursePostId,
1819
discourseApiKey,
1920
discourseUser,
2021
commit,
21-
specFile
22+
specFile,
23+
bodyTemplate
2224
)

0 commit comments

Comments
 (0)