ci: coverage-action
This commit is contained in:
parent
c51d4d23a6
commit
de8ec121dc
9
dev/coverage-action/.editorconfig
Normal file
9
dev/coverage-action/.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
4
dev/coverage-action/.eslintrc
Normal file
4
dev/coverage-action/.eslintrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": "standard"
|
||||
}
|
2
dev/coverage-action/.gitignore
vendored
Normal file
2
dev/coverage-action/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
/data
|
2
dev/coverage-action/.npmrc
Normal file
2
dev/coverage-action/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
save-exact = true
|
||||
save-prefix = ""
|
42
dev/coverage-action/action.yml
Normal file
42
dev/coverage-action/action.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: 'Datatracker Coverage + Changelog Parser'
|
||||
description: 'Parse and generate coverage and changelog files for Datatracker releases'
|
||||
author: Nicolas Giard
|
||||
inputs:
|
||||
token:
|
||||
description: GitHub Token
|
||||
required: true
|
||||
tokenCommon:
|
||||
description: GitHub Token for Common repository
|
||||
required: true
|
||||
repoCommon:
|
||||
description: Common repository containing the SVG coverage charts
|
||||
required: false
|
||||
default: 'common'
|
||||
version:
|
||||
description: Release version
|
||||
required: true
|
||||
changelog:
|
||||
description: Generated changelog content from Changelog Action
|
||||
required: true
|
||||
default: ''
|
||||
summary:
|
||||
description: Summary to prepend to the changelog body
|
||||
required: false
|
||||
default: ''
|
||||
coverageResultsPath:
|
||||
description: Path to the latest coverage results file
|
||||
required: true
|
||||
default: 'coverage.json'
|
||||
histCoveragePath:
|
||||
description: Path where to output the historical coverage file
|
||||
required: true
|
||||
default: 'historical-coverage.json'
|
||||
outputs:
|
||||
changelog:
|
||||
description: Changelog with headers prepended and coverage stats + chart appended
|
||||
runs:
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
branding:
|
||||
icon: layers
|
||||
color: red
|
13269
dev/coverage-action/dist/chart.js
vendored
Normal file
13269
dev/coverage-action/dist/chart.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
37657
dev/coverage-action/dist/index.js
vendored
Normal file
37657
dev/coverage-action/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
246
dev/coverage-action/fix-old-releases.js
Normal file
246
dev/coverage-action/fix-old-releases.js
Normal file
|
@ -0,0 +1,246 @@
|
|||
const github = require('@actions/github')
|
||||
const fs = require('fs/promises')
|
||||
const isPlainObject = require('lodash/isPlainObject')
|
||||
const orderBy = require('lodash/orderBy')
|
||||
const find = require('lodash/find')
|
||||
const slice = require('lodash/slice')
|
||||
const round = require('lodash/round')
|
||||
const { ChartJSNodeCanvas } = require('chartjs-node-canvas')
|
||||
|
||||
const chartJSNodeCanvas = new ChartJSNodeCanvas({ type: 'svg', width: 850, height: 300, backgroundColour: '#FFFFFF' })
|
||||
|
||||
async function main () {
|
||||
const token = 'YOUR_TOKEN_HERE'
|
||||
const gh = github.getOctokit(token)
|
||||
const owner = 'ietf-tools'
|
||||
const repo = 'datatracker'
|
||||
const repoCommon = 'common'
|
||||
|
||||
// -> Fetch existing releases
|
||||
const releases = []
|
||||
let hasMoreReleases = false
|
||||
let releasesCurPage = 0
|
||||
do {
|
||||
hasMoreReleases = false
|
||||
releasesCurPage++
|
||||
const resp = await gh.request('GET /repos/{owner}/{repo}/releases', {
|
||||
owner,
|
||||
repo,
|
||||
page: releasesCurPage,
|
||||
per_page: 100
|
||||
})
|
||||
if (resp?.data?.length > 0) {
|
||||
console.info(`Fetching existing releases... ${(releasesCurPage - 1) * 100}`)
|
||||
hasMoreReleases = true
|
||||
releases.push(...resp.data)
|
||||
}
|
||||
} while (hasMoreReleases)
|
||||
console.info(`Found ${releases.length} existing releases.`)
|
||||
|
||||
// -> Load full coverage file
|
||||
console.info('Loading coverage results file...')
|
||||
const rawCoverage = await fs.readFile('data/release-coverage.json', 'utf8')
|
||||
const coverage = JSON.parse(rawCoverage)
|
||||
|
||||
// -> Parse and reorder results
|
||||
const versions = []
|
||||
for (const [key, value] of Object.entries(coverage)) {
|
||||
if (isPlainObject(value)) {
|
||||
versions.push({
|
||||
tag: key,
|
||||
time: value?.time,
|
||||
stats: {
|
||||
code: value?.code?.coverage || 0,
|
||||
template: value?.template?.coverage || 0,
|
||||
url: value?.url?.coverage || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const oVersions = orderBy(versions, ['time', 'tag'], ['desc', 'desc'])
|
||||
const roVersions = orderBy(versions, ['time', 'tag'], ['asc', 'asc'])
|
||||
|
||||
// -> Fetch list of existing chart files in common repo
|
||||
console.info('Fetching list of existing chart files from common repo...')
|
||||
const chartsDirListing = []
|
||||
const respDir = await gh.request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner,
|
||||
repo: repoCommon,
|
||||
path: 'assets/graphs/datatracker'
|
||||
})
|
||||
if (respDir?.data?.length > 0) {
|
||||
chartsDirListing.push(...respDir.data)
|
||||
}
|
||||
|
||||
// -> Upload release coverage
|
||||
for (const [idx, value] of oVersions.entries()) {
|
||||
const rel = find(releases, ['tag_name', value.tag])
|
||||
if (!rel) { continue }
|
||||
|
||||
// -> Full Coverage File
|
||||
if (rel?.assets?.some(a => a.name === 'coverage.json')) {
|
||||
console.info(`Coverage file already exists for ${value.tag}, skipping...`)
|
||||
} else {
|
||||
console.info(`Building coverage object for ${value.tag}...`)
|
||||
const covData = Buffer.from(JSON.stringify({
|
||||
[value.tag]: coverage[value.tag],
|
||||
version: value.tag
|
||||
}), 'utf8')
|
||||
|
||||
console.info(`Uploading coverage file for ${value.tag}...`)
|
||||
await gh.rest.repos.uploadReleaseAsset({
|
||||
data: covData,
|
||||
owner,
|
||||
repo,
|
||||
release_id: rel.id,
|
||||
name: 'coverage.json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// -> Historical Coverage File
|
||||
if (rel?.assets?.some(a => a.name === 'historical-coverage.json')) {
|
||||
console.info(`Historical Coverage file already exists for ${value.tag}, skipping...`)
|
||||
} else {
|
||||
console.info(`Building historical coverage object for ${value.tag}...`)
|
||||
const final = {}
|
||||
for (const obj of slice(oVersions, idx)) {
|
||||
final[obj.tag] = obj.stats
|
||||
}
|
||||
|
||||
const covData = Buffer.from(JSON.stringify(final), 'utf8')
|
||||
|
||||
console.info(`Uploading historical coverage file for ${value.tag}...`)
|
||||
await gh.rest.repos.uploadReleaseAsset({
|
||||
data: covData,
|
||||
owner,
|
||||
repo,
|
||||
release_id: rel.id,
|
||||
name: 'historical-coverage.json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// -> Coverage Chart
|
||||
if (chartsDirListing.some(c => c.name === `${rel.id}.svg`)) {
|
||||
console.info(`Chart SVG already exists for ${rel.name}, skipping...`)
|
||||
} else {
|
||||
console.info(`Generating chart SVG for ${rel.name}...`)
|
||||
const labels = []
|
||||
const datasetCode = []
|
||||
const datasetTemplate = []
|
||||
const datasetUrl = []
|
||||
for (const obj of slice(roVersions, 0, roVersions.length - idx)) {
|
||||
labels.push(obj.tag)
|
||||
datasetCode.push(round(obj.stats.code * 100, 2))
|
||||
datasetTemplate.push(round(obj.stats.template * 100, 2))
|
||||
datasetUrl.push(round(obj.stats.url * 100, 2))
|
||||
}
|
||||
|
||||
const outputStream = chartJSNodeCanvas.renderToBufferSync({
|
||||
type: 'line',
|
||||
options: {
|
||||
borderColor: '#CCC',
|
||||
layout: {
|
||||
padding: 20
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: {
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
callback: (value) => {
|
||||
return `${value}%`
|
||||
},
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Code',
|
||||
data: datasetCode,
|
||||
borderWidth: 2,
|
||||
borderColor: '#E53935',
|
||||
backgroundColor: '#C6282833',
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'Templates',
|
||||
data: datasetTemplate,
|
||||
borderWidth: 2,
|
||||
borderColor: '#039BE5',
|
||||
backgroundColor: '#0277BD33',
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'URLs',
|
||||
data: datasetUrl,
|
||||
borderWidth: 2,
|
||||
borderColor: '#7CB342',
|
||||
backgroundColor: '#558B2F33',
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}, 'image/svg+xml')
|
||||
const svg = Buffer.from(outputStream).toString('base64')
|
||||
|
||||
console.info(`Uploading chart SVG for ${rel.name}...`)
|
||||
await gh.rest.repos.createOrUpdateFileContents({
|
||||
owner,
|
||||
repo: repoCommon,
|
||||
path: `assets/graphs/datatracker/${rel.id}.svg`,
|
||||
message: `chore: update datatracker release chart for release ${rel.name}`,
|
||||
content: svg
|
||||
})
|
||||
}
|
||||
|
||||
if (rel.body.includes(`${rel.id}.svg`)) {
|
||||
console.info(`Release ${rel.name} body already contains the chart SVG, skipping...`)
|
||||
} else {
|
||||
console.info(`Appending chart SVG to release ${rel.name} body...`)
|
||||
await gh.request('PATCH /repos/{owner}/{repo}/releases/{release_id}', {
|
||||
owner,
|
||||
repo,
|
||||
release_id: rel.id,
|
||||
body: `${rel.body}\r\n\r\n`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
262
dev/coverage-action/index.js
Normal file
262
dev/coverage-action/index.js
Normal file
|
@ -0,0 +1,262 @@
|
|||
const github = require('@actions/github')
|
||||
const core = require('@actions/core')
|
||||
const orderBy = require('lodash/orderBy')
|
||||
const find = require('lodash/find')
|
||||
const round = require('lodash/round')
|
||||
const fs = require('fs/promises')
|
||||
const { DateTime } = require('luxon')
|
||||
|
||||
const dec = new TextDecoder()
|
||||
|
||||
async function main () {
|
||||
const token = core.getInput('token')
|
||||
const inputCovPath = core.getInput('coverageResultsPath') // 'data/coverage-raw.json'
|
||||
const outputCovPath = core.getInput('coverageResultsPath') // 'data/coverage.json'
|
||||
const outputHistPath = core.getInput('histCoveragePath') // 'data/historical-coverage.json'
|
||||
const relVersionRaw = core.getInput('version') // 'v7.47.0'
|
||||
const relVersion = relVersionRaw.indexOf('v') === 0 ? relVersionRaw.substring(1) : relVersionRaw
|
||||
const gh = github.getOctokit(token)
|
||||
const owner = github.context.repo.owner // 'ietf-tools'
|
||||
const repo = github.context.repo.repo // 'datatracker'
|
||||
const sender = github.context.payload.sender.login // 'rjsparks'
|
||||
const repoCommon = core.getInput('repoCommon') // 'common'
|
||||
const summary = core.getInput('summary') // ''
|
||||
const changelog = core.getInput('changelog') // ''
|
||||
|
||||
// -> Parse coverage results
|
||||
console.info('Parsing coverage.json file...')
|
||||
const covLatest = JSON.parse(await fs.readFile(inputCovPath, 'utf8'))
|
||||
const covLatestStats = {
|
||||
code: covLatest.latest.code.coverage,
|
||||
template: covLatest.latest.template.coverage,
|
||||
url: covLatest.latest.url.coverage
|
||||
}
|
||||
|
||||
// -> Fix coverage results versioning
|
||||
console.info(`Writing ${relVersion} normalized coverage.json file...`)
|
||||
fs.writeFile(outputCovPath, JSON.stringify({
|
||||
[relVersion]: covLatest.latest,
|
||||
version: relVersion
|
||||
}, null, 2), 'utf8')
|
||||
|
||||
// -> Fetch existing releases
|
||||
const releases = []
|
||||
let hasMoreReleases = false
|
||||
let releasesCurPage = 0
|
||||
do {
|
||||
hasMoreReleases = false
|
||||
releasesCurPage++
|
||||
const resp = await gh.request('GET /repos/{owner}/{repo}/releases', {
|
||||
owner,
|
||||
repo,
|
||||
page: releasesCurPage,
|
||||
per_page: 100
|
||||
})
|
||||
if (resp?.data?.length > 0) {
|
||||
console.info(`Fetching existing releases... ${(releasesCurPage - 1) * 100}`)
|
||||
hasMoreReleases = true
|
||||
releases.push(...resp.data)
|
||||
}
|
||||
} while (hasMoreReleases)
|
||||
console.info(releases[0])
|
||||
console.info(`Found ${releases.length} existing releases.`)
|
||||
|
||||
// -> Fetch latest historical coverage
|
||||
let covData = null
|
||||
for (const rel of orderBy(releases, ['created_at'], ['desc'])) {
|
||||
if (rel.draft) { continue }
|
||||
|
||||
const covAsset = find(rel.assets, ['name', 'historical-coverage.json'])
|
||||
if (covAsset) {
|
||||
console.info(`Fetching latest historical-coverage.json from release ${rel.name}...`)
|
||||
const covRaw = await gh.request('GET /repos/{owner}/{repo}/releases/assets/{asset_id}', {
|
||||
owner,
|
||||
repo,
|
||||
asset_id: covAsset.id,
|
||||
headers: {
|
||||
Accept: 'application/octet-stream'
|
||||
}
|
||||
})
|
||||
covData = JSON.parse(dec.decode(covRaw.data))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// -> Update historical coverage
|
||||
if (!covData) {
|
||||
console.warn('Could not find historical coverage data... Skipping...')
|
||||
} else {
|
||||
console.info('Writing updated historical-coverage.json file...')
|
||||
covData = {
|
||||
[relVersion]: covLatestStats,
|
||||
...covData
|
||||
}
|
||||
await fs.writeFile(outputHistPath, JSON.stringify(covData, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
// -> Find matching release version
|
||||
const newRelease = find(releases, ['name', 'v7.46.0']) // relVersionRaw
|
||||
if (!newRelease) {
|
||||
console.warn(`Could not find a release matching ${relVersionRaw}... Skipping coverage chart generation...`)
|
||||
return
|
||||
}
|
||||
|
||||
// -> Fetch list of existing chart files in common repo
|
||||
console.info('Fetching list of existing chart files from common repo...')
|
||||
const chartsDirListing = []
|
||||
const respDir = await gh.request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner,
|
||||
repo: repoCommon,
|
||||
path: 'assets/graphs/datatracker'
|
||||
})
|
||||
if (respDir?.data?.length > 0) {
|
||||
chartsDirListing.push(...respDir.data)
|
||||
}
|
||||
|
||||
// -> Coverage Chart
|
||||
if (chartsDirListing.some(c => c.name === `${newRelease.id}.svg`)) {
|
||||
console.info(`Chart SVG already exists for ${newRelease.name}, skipping...`)
|
||||
} else {
|
||||
console.info(`Generating chart SVG for ${newRelease.name}...`)
|
||||
|
||||
const { ChartJSNodeCanvas } = require('chartjs-node-canvas')
|
||||
const chartJSNodeCanvas = new ChartJSNodeCanvas({ type: 'svg', width: 850, height: 300, backgroundColour: '#FFFFFF' })
|
||||
|
||||
const labels = []
|
||||
const datasetCode = []
|
||||
const datasetTemplate = []
|
||||
const datasetUrl = []
|
||||
for (const [key, value] of Object.entries(covData)) {
|
||||
labels.push(key)
|
||||
datasetCode.push(round(value.code * 100, 2))
|
||||
datasetTemplate.push(round(value.template * 100, 2))
|
||||
datasetUrl.push(round(value.url * 100, 2))
|
||||
}
|
||||
|
||||
const outputStream = chartJSNodeCanvas.renderToBufferSync({
|
||||
type: 'line',
|
||||
options: {
|
||||
borderColor: '#CCC',
|
||||
layout: {
|
||||
padding: 20
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: {
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
callback: (value) => {
|
||||
return `${value}%`
|
||||
},
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Code',
|
||||
data: datasetCode,
|
||||
borderWidth: 2,
|
||||
borderColor: '#E53935',
|
||||
backgroundColor: '#C6282833',
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'Templates',
|
||||
data: datasetTemplate,
|
||||
borderWidth: 2,
|
||||
borderColor: '#039BE5',
|
||||
backgroundColor: '#0277BD33',
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'URLs',
|
||||
data: datasetUrl,
|
||||
borderWidth: 2,
|
||||
borderColor: '#7CB342',
|
||||
backgroundColor: '#558B2F33',
|
||||
fill: false,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
tension: 0.4,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}, 'image/svg+xml')
|
||||
const svg = Buffer.from(outputStream).toString('base64')
|
||||
|
||||
console.info(`Uploading chart SVG for ${newRelease.name}...`)
|
||||
await gh.rest.repos.createOrUpdateFileContents({
|
||||
owner,
|
||||
repo: repoCommon,
|
||||
path: `assets/graphs/datatracker/${newRelease.id}.svg`,
|
||||
message: `chore: update datatracker release chart for release ${newRelease.name}`,
|
||||
content: svg
|
||||
})
|
||||
}
|
||||
|
||||
// -> Add to changelog body
|
||||
let formattedBody = ''
|
||||
const covInfo = {
|
||||
code: round(covLatestStats.code * 100, 2),
|
||||
template: round(covLatestStats.template * 100, 2),
|
||||
url: round(covLatestStats.url * 100, 2)
|
||||
}
|
||||
|
||||
formattedBody = summary ? `**Summary:** ${summary}\n` : ''
|
||||
formattedBody += `**Release Date**: ${DateTime.now().setZone('utc').toFormat('ccc, LLLL d, y \'at\' h:mm a ZZZZ')}\n`
|
||||
formattedBody += `**Release Author**: @${sender}\n`
|
||||
formattedBody += '\n---\n\n'
|
||||
formattedBody += changelog
|
||||
formattedBody += '\n\n---\n\n**Coverage**\n\n'
|
||||
formattedBody += `}?style=flat-square)`
|
||||
formattedBody += `}?style=flat-square)`
|
||||
formattedBody += `}?style=flat-square)\n\n`
|
||||
formattedBody += ``
|
||||
|
||||
core.exportVariable('changelog', formattedBody)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
function getCoverageColor (val) {
|
||||
if (val >= 95) {
|
||||
return 'brightgreen'
|
||||
} else if (val >= 90) {
|
||||
return 'green'
|
||||
} else if (val >= 80) {
|
||||
return 'yellowgreen'
|
||||
} else if (val >= 60) {
|
||||
return 'yellow'
|
||||
} else if (val >= 50) {
|
||||
return 'orange'
|
||||
} else {
|
||||
return 'red'
|
||||
}
|
||||
}
|
4670
dev/coverage-action/package-lock.json
generated
Normal file
4670
dev/coverage-action/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
dev/coverage-action/package.json
Normal file
27
dev/coverage-action/package.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "coverage-action",
|
||||
"version": "1.0.0",
|
||||
"description": "Parse and generate coverage and changelog files for Datatracker releases",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "ncc build index.js -o dist"
|
||||
},
|
||||
"author": "IETF Trust",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.6.0",
|
||||
"@actions/github": "5.0.0",
|
||||
"chart.js": "3.7.1",
|
||||
"chartjs-node-canvas": "4.1.6",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "0.33.3",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-standard": "16.0.3",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "5.2.0"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue