Skip to content

Commit df76e67

Browse files
committed
feat: add custom node action to check all meson files for correct formatting
1 parent ae1487e commit df76e67

File tree

6 files changed

+395
-14
lines changed

6 files changed

+395
-14
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: 'Meson Format Action'
2+
author: Totto16
3+
description: 'Check if meson files are formatted correctly'
4+
inputs:
5+
format-file:
6+
description: 'Path of format file'
7+
required: false
8+
default: ''
9+
only-git-files:
10+
description: 'Only check files, that are check into git'
11+
required: false
12+
default: 'true'
13+
runs:
14+
using: 'composite'
15+
steps:
16+
- name: Install Node.Js
17+
uses: actions/setup-node@v4
18+
id: setup-js
19+
with:
20+
node-version: 22
21+
22+
- name: Install Node Dependencies
23+
id: install-dependencies
24+
shell: bash
25+
run: |
26+
npm ci
27+
28+
# NOTE: meson has no dependencies, so --break-system-packages doesn't really break anything!
29+
- name: Setup meson
30+
id: install-meson
31+
shell: bash
32+
run: |
33+
pip install meson --break-system-packages
34+
35+
- name: Run checker
36+
shell: bash
37+
id: meson-format-checker
38+
run: |
39+
node index.js "${{ inputs.format-file }}" "${{ inputs.only-git-files }}"
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
const core = require('@actions/core')
2+
const exec = require('@actions/exec')
3+
const io = require('@actions/io')
4+
5+
/**
6+
* @callback ExecListenerCallback
7+
* @param {Buffer} data
8+
* @returns {void}
9+
*/
10+
11+
/**
12+
* @typedef ExecListeners
13+
* @type {object}
14+
* @property {ExecListenerCallback} stdout
15+
*/
16+
17+
/**
18+
* @typedef ExecOptions
19+
* @type {object}
20+
* @property {ExecListeners} listeners
21+
* @property {boolean} failOnStdErr
22+
* @property {boolean} ignoreReturnCode
23+
*/
24+
25+
/**
26+
* @typedef AnnotationProperties
27+
* @type {object}
28+
* @property {string} title
29+
* @property {string} file
30+
/**
31+
*
32+
* @param {string} executable
33+
* @param {string[]} arguments
34+
* @async
35+
* @returns {Promise<string[]>}
36+
*/
37+
async function execAndGetStdout(executable, args) {
38+
/** @type {string} */
39+
let output = ''
40+
41+
/** @type {ExecOptions} */
42+
const options = {
43+
failOnStdErr: true,
44+
ignoreReturnCode: false,
45+
}
46+
47+
options.listeners = {
48+
stdout: (data) => {
49+
output += data.toString()
50+
},
51+
}
52+
53+
await io.which(executable, true)
54+
55+
/** @type {number} */
56+
const exitCode = await exec.exec(executable, args, options)
57+
58+
if (exitCode != 0) {
59+
throw new Error(`${executable} exited with exit code ${exitCode}`)
60+
}
61+
62+
return output.split('\n')
63+
}
64+
65+
/**
66+
* @param {boolean} onlyGitFiles
67+
* @async
68+
* @returns {Promise<string[]>}
69+
*/
70+
async function getMesonFiles(onlyGitFiles) {
71+
/** @type {string[]} */
72+
let files
73+
74+
/** @type {string[]} */
75+
const mesonFiles = ['meson.build', 'meson.options', 'meson_options.txt']
76+
77+
if (onlyGitFiles) {
78+
/** @type {string[]} */
79+
const gitFiles = mesonFiles.map((file) => `--exclude='${file}'`)
80+
81+
files = await execAndGetStdout('git', [
82+
'ls-files',
83+
...gitFiles,
84+
'--ignored',
85+
'-c',
86+
])
87+
} else {
88+
/** @type {string[][]} */
89+
const findFiles = mesonFiles.map((file) => ['-name', file])
90+
91+
/** @type {string[]} */
92+
const finalFindFiles = findFiles.reduce((acc, elem, index) => {
93+
if (index != 0) {
94+
acc.push('-o')
95+
}
96+
97+
acc.push(...elem)
98+
99+
return acc
100+
}, [])
101+
102+
files = await execAndGetStdout('find', [
103+
'.',
104+
'(',
105+
...finalFindFiles,
106+
')',
107+
])
108+
}
109+
110+
return files
111+
}
112+
113+
/**
114+
*
115+
* @param {string} file
116+
* @param {string} formatFile
117+
* @async
118+
* @returns {Promise<boolean>}
119+
*/
120+
async function checkFile(file, formatFile) {
121+
/** @type {ExecOptions} */
122+
const options = {
123+
ignoreReturnCode: true,
124+
}
125+
126+
/** @type {string[]} */
127+
const additionalArgs = formatFile == '' ? [] : ['-c', formatFile]
128+
129+
/** @type {number} */
130+
const exitCode = await exec.exec(
131+
'meson',
132+
['format', '--check-only', ...additionalArgs, file],
133+
options
134+
)
135+
136+
return exitCode == 0
137+
}
138+
139+
/**
140+
* @async
141+
* @returns {Promise<void>}
142+
*/
143+
async function main() {
144+
try {
145+
/** @type {string} */
146+
const os = core.platform.platform
147+
148+
if (os != 'linux') {
149+
throw new Error(
150+
`Action atm only supported on linux: but are on: ${os}`
151+
)
152+
}
153+
154+
/** @type {string} */
155+
const formatFile = core.getInput('format-file', { required: false })
156+
157+
/** @type {boolean} */
158+
const onlyGitFiles = core.getBooleanInput('only-git-files', {
159+
required: false,
160+
})
161+
162+
/** @type {string[]} */
163+
const files = await getMesonFiles(onlyGitFiles)
164+
165+
await io.which('meson', true)
166+
167+
/** @type {string[]} */
168+
const notFormattedFiles = []
169+
170+
core.startGroup('Check all files')
171+
172+
//TODO: maybe parallelize this
173+
for (const file of files) {
174+
core.info(`Checking file: '${file}'`)
175+
176+
/** @type {boolean} */
177+
const result = await checkFile(file, formatFile)
178+
179+
core.info(
180+
result
181+
? 'File is formatted correctly'
182+
: 'File has formatting errors'
183+
)
184+
core.info('')
185+
186+
if (!result) {
187+
notFormattedFiles.push(file)
188+
189+
/** @type {AnnotationProperties} */
190+
const properties = {
191+
file,
192+
title: 'File not formatted correctly',
193+
}
194+
195+
core.error('File not formatted correctly', properties)
196+
}
197+
}
198+
199+
core.endGroup()
200+
201+
if (notFormattedFiles.length == 0) {
202+
core.summary.clear()
203+
204+
core.summary.addHeading('Result', 1)
205+
core.summary.addRaw(
206+
':white_check_mark: All files are correctly formatted',
207+
true
208+
)
209+
210+
core.summary.write()
211+
return
212+
}
213+
214+
core.summary.clear()
215+
216+
core.summary.addList(
217+
notFormattedFiles.map((file) => `\`${file}\``),
218+
false
219+
)
220+
221+
/** @type {string} */
222+
const fileList = core.summary.stringify()
223+
core.summary.clear()
224+
225+
core.summary.addHeading('Result', 1)
226+
core.summary.addRaw(':x: Some files are not formatted correctly', true)
227+
228+
core.summary.addDetails('Affected Files', fileList)
229+
core.summary.addSeparator()
230+
231+
core.summary.addRaw(
232+
'To format the files run the following command',
233+
true
234+
)
235+
236+
/** @type {string} */
237+
const additionalArgs = formatFile == '' ? [] : [`-c ${formatFile}`]
238+
239+
/** @type {string} */
240+
const finalFileList = notFormattedFiles.join(' ')
241+
242+
core.summary.addCodeBlock(
243+
`meson format ${additionalArgs} -i ${finalFileList}`,
244+
'bash'
245+
)
246+
247+
core.summary.write()
248+
249+
throw new Error('Soem files are not formatted correctly')
250+
} catch (error) {
251+
core.setFailed(error)
252+
}
253+
}
254+
255+
main()

.github/actions/meson-format-action/package-lock.json

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "meson-format-action",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"author": "Totto16",
6+
"license": "MIT",
7+
"description": "",
8+
"dependencies": {
9+
"@actions/core": "^1.11.1",
10+
"@actions/exec": "^1.1.1",
11+
"@actions/io": "^1.1.3"
12+
}
13+
}

0 commit comments

Comments
 (0)