Skip to content

Commit 7762c81

Browse files
committed
feat: add action code
1 parent ae8b474 commit 7762c81

File tree

4 files changed

+191
-18
lines changed

4 files changed

+191
-18
lines changed

action.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Label Sync
2+
description: An action that allows you to sync labels from a repository or a config file
3+
author: Federico Grandi <fgrandi30@gmail.com>
4+
5+
runs:
6+
using: node12
7+
main: lib/index.js
8+
9+
inputs:
10+
token:
11+
description: The token needed to edit the labels in this repo
12+
required: true
13+
14+
config-file:
15+
description: The path to the JSON or YAML file containing the label config (more info in the README)
16+
required: false
17+
source-repo:
18+
description: The repo to copy labels from (if not using a config file), in the 'owner/repo' format
19+
required: false
20+
source-repo-token:
21+
description: A token to use if the source repo is private
22+
required: false
23+
24+
delete-other-labels:
25+
description: Whether to delete any other label (useful when setting up a new repo, dangerous when editing an existing one)
26+
required: false
27+
default: 'false'
28+
dry-run:
29+
description: Whether to only display the changes, without making them (useful if you're worried you're going to mess up)
30+
required: false
31+
default: 'false'

package-lock.json

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

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@
2323
"homepage": "https://github.com/EndBug/label-sync#readme",
2424
"dependencies": {
2525
"@actions/core": "^1.2.6",
26-
"github-label-sync": "github:EndBug/github-label-sync#typings-usable"
26+
"axios": "^0.20.0",
27+
"github-label-sync": "github:EndBug/github-label-sync#typings-usable",
28+
"yamljs": "^0.3.0"
2729
},
2830
"devDependencies": {
2931
"@types/node": "^14.11.5",
32+
"@types/yamljs": "^0.2.31",
3033
"@typescript-eslint/eslint-plugin": "^4.4.0",
3134
"@typescript-eslint/parser": "^4.4.0",
3235
"@vercel/ncc": "^0.24.1",
3336
"eslint": "^7.10.0",
3437
"husky": "^4.3.0",
3538
"ts-node": "^9.0.0",
36-
"typescript": "^4.0.3",
37-
"yamljs": "^0.3.0"
39+
"typescript": "^4.0.3"
3840
}
3941
}

src/index.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import core, { getInput, setFailed } from '@actions/core'
2+
import githubLabelSync, { LabelInfo } from 'github-label-sync'
3+
import fs from 'fs'
4+
import path from 'path'
5+
import yaml from 'yamljs'
6+
import axios from 'axios'
7+
8+
let usingLocalFile: boolean
9+
10+
(async () => {
11+
try {
12+
checkInputs()
13+
14+
const labels = usingLocalFile
15+
? readConfigFile(getInput('config-file'))
16+
: await fetchRepoLabels(getInput('source-repo'), getInput('source-repo-token'))
17+
18+
const diff = await githubLabelSync({
19+
accessToken: getInput('token'),
20+
repo: process.env.GITHUB_REPOSITORY as string,
21+
labels,
22+
23+
allowAddedLabels: getInput('delete-other-labels') != 'true',
24+
dryRun: getInput('dry-run') == 'true'
25+
})
26+
27+
core.startGroup('Label diff')
28+
core.info(JSON.stringify(diff, null, 2))
29+
core.endGroup()
30+
} catch (e) { setFailed(e) }
31+
})
32+
33+
function isProperConfig(value: any): value is LabelInfo[] {
34+
return value instanceof Array
35+
&& value.every(element => (
36+
typeof element == 'object'
37+
&& element.name && typeof element.name == 'string'
38+
&& element.description && typeof element.description == 'string'
39+
&& (
40+
!element.aliases
41+
|| (
42+
element.aliases instanceof Array
43+
&& element.aliases.every(alias => alias && typeof alias == 'string')
44+
)
45+
)
46+
&& (
47+
!element.description
48+
|| typeof element.description == 'string'
49+
)
50+
))
51+
}
52+
53+
function readConfigFile(filePath: string) {
54+
let file: string
55+
56+
try { // Read the file from the given path
57+
file = fs.readFileSync(path.resolve(filePath), { encoding: 'utf-8' })
58+
if (!file || typeof file != 'string') throw null
59+
} catch {
60+
throw 'Can\'t access config file.'
61+
}
62+
63+
let parsed: LabelInfo[]
64+
const fileExtension = path.extname(filePath).toLowerCase()
65+
66+
if (['.yaml', '.yml'].includes(fileExtension)) {
67+
// Parse YAML file
68+
parsed = yaml.parse(file)
69+
if (!isProperConfig(parsed))
70+
throw `Parsed YAML file is invalid. Parsed: ${JSON.stringify(parsed, null, 2)}`
71+
} else if (fileExtension == '.json') {
72+
// Try to parse JSON file
73+
try {
74+
parsed = JSON.parse(file)
75+
} catch {
76+
throw 'Couldn\'t parse JSON config file, check for syntax errors.'
77+
}
78+
if (!isProperConfig(parsed))
79+
throw `Parsed JSON file is invalid. Parsed: ${JSON.stringify(parsed, null, 2)}`
80+
} else {
81+
throw `Invalid file extension: ${fileExtension}`
82+
}
83+
84+
return parsed
85+
}
86+
87+
async function fetchRepoLabels(repo: string, token?: string): Promise<LabelInfo[]> {
88+
core.startGroup('Getting repo labels...')
89+
90+
const url = `https://api.github.com/repos/${repo}/labels`,
91+
headers = token ? { Authorization: `token ${token}` } : undefined
92+
core.info(`Using following URL: ${url}`)
93+
94+
const { data } = await axios.get(url, { headers })
95+
if (!data || !(data instanceof Array))
96+
throw 'Can\'t get label data from GitHub API'
97+
98+
core.info(`${data.length} labels fetched.`)
99+
core.endGroup()
100+
101+
return data.map(element => ({
102+
name: element.name as string,
103+
color: element.color as string,
104+
description: element.description as string || undefined,
105+
// Can't fetch aliases from a source repo
106+
}))
107+
}
108+
109+
function checkInputs() {
110+
if (!getInput('token'))
111+
setFailed('The token parameter is required.')
112+
113+
const configFile = getInput('config-file'),
114+
sourceRepo = getInput('source-repo')
115+
116+
if (!!configFile == !!sourceRepo)
117+
setFailed('You can\'t use a config file and a source repo at the same time. Choose one!')
118+
119+
// config-file: doesn't need evaluation, will be evaluated when parsing
120+
usingLocalFile = !!configFile
121+
122+
if (sourceRepo && sourceRepo.split('/').length != 2)
123+
setFailed('Source repo should be in the owner/repo format, like EndBug/label-sync!')
124+
if (sourceRepo && !getInput('source-repo-token'))
125+
core.info('You\'re using a source repo without a token: if your repository is private the action won\'t be able to read the labels.')
126+
127+
if (!['true', 'false'].includes(getInput('delete-other-labels')))
128+
setFailed('The only values you can use for the `delete-other-labels` option are `true` and `false`')
129+
if (!['true', 'false'].includes(getInput('dry-run')))
130+
setFailed('The only values you can use for the `dry-run` option are `true` and `false`')
131+
}

0 commit comments

Comments
 (0)