Skip to content

Commit 9d317a2

Browse files
feat!: add option to use remote config files (#2)
* add option to use remote config files * add remoteConfig branch logic * add missed out token parameter * fix: use urls instead of repo+path * chore: add debug info * chore: add debug info * chore: npm audit fix * fix: minor typo * docs: expand documentation Co-authored-by: Federico Grandi <[email protected]>
1 parent baf556b commit 9d317a2

File tree

5 files changed

+142
-35
lines changed

5 files changed

+142
-35
lines changed

README.md

+41-9
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ jobs:
1515
runs-on: ubuntu-latest
1616

1717
steps:
18-
- uses: EndBug/label-sync@v1
18+
- uses: EndBug/label-sync@v2
1919
with:
20-
# If you want to use a config file, you can put its path here (more info in the paragraphs below)
21-
config-file: .github/labels.yml
20+
# If you want to use a config file, you can put its path or URL here (more info in the paragraphs below)
21+
config-file:
22+
.github/labels.yml
23+
# If URL: "https://raw.githubusercontent.com/EndBug/label-sync/main/.github/labels.yml"
2224

2325
# If you want to use a source repo, you can put is name here (only the owner/repo format is accepted)
2426
source-repo: owner/repo
25-
# If you're using a private source repo, you'll need to add a custom token for the action to read it
26-
source-repo-token: ${{ secrets.YOUR_OWN_SECRET }}
27+
28+
# If you're using a private source repo or a URL that needs an 'Authorization' header, you'll need to add a custom token for the action to read it
29+
request-token: ${{ secrets.YOUR_OWN_SECRET }}
2730

2831
# If you want to delete any additional label, set this to true
2932
delete-other-labels: false
@@ -57,15 +60,15 @@ This is how it would end up looking:
5760

5861
```yaml
5962
- name: A label
60-
color: "000000"
63+
color: '000000'
6164
6265
- name: Another label
63-
color: "111111"
66+
color: '111111'
6467
description: A very inspiring description
6568
6669
- name: Yet another label
67-
color: "222222"
68-
aliases: ["first", "second", "third"]
70+
color: '222222'
71+
aliases: ['first', 'second', 'third']
6972
```
7073

7174
```json
@@ -88,3 +91,32 @@ This is how it would end up looking:
8891
```
8992

9093
If you want to see an actual config file, you can check out the one in this repo [here](.github/labels.yml).
94+
95+
This action can either read a local file or fetch it from a custom URL.
96+
If you want to use a URL make sure that the data field of the response contains JSON or YAML text that follows the structure above.
97+
98+
An example of how you may want to use a URL instead of a local file is if you want to use a config file that is located in a GitHub repo, without having to copy it to your own.
99+
You can use the "raw" link that GitHub provides for the file:
100+
101+
```yaml
102+
- uses: EndBug/label-sync@v2
103+
with:
104+
# This is just an example, but any valid URL can be used
105+
config-file: 'https://raw.githubusercontent.com/EndBug/label-sync/main/.github/labels.yml'
106+
```
107+
108+
This is different than using the `source-repo` option, since this also allows you to use aliases, if the config file has any. If you use the `source-repo` option the action will only copy over the missing labels and update colors, wihtout updating or deleting anything else.
109+
110+
If the URL you're using needs an `Authorization` header (like if, for example, you're fetching it from a private repo), you can put its value in the `request-token` input:
111+
112+
```yaml
113+
- uses: EndBug/label-sync@v2
114+
with:
115+
config-file: 'https://raw.githubusercontent.com/User/repo-name/path/to/labels.yml'
116+
# Remember not to put PATs in files, use GitHub secrets instead
117+
request-token: ${{ secrets.YOUR_CUSTOM_PAT }}
118+
```
119+
120+
The `request-token` input can also be used with a `source-repo`, if that repo is private.
121+
122+
If your URL needs a more elaborate request, it's better if you perform it separately and save its output to a local file. You can then run the action using the local config file you just created.

action.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ inputs:
1313
default: ${{ github.token }}
1414

1515
config-file:
16-
description: The path to the JSON or YAML file containing the label config (more info in the README)
16+
description: The path (or URL) to the JSON or YAML file containing the label config (more info in the README)
1717
required: false
1818
source-repo:
1919
description: The repo to copy labels from (if not using a config file), in the 'owner/repo' format
2020
required: false
21-
source-repo-token:
22-
description: A token to use if the source repo is private
21+
request-token:
22+
description: The token to use in the 'Authorization' header (if 'config-file' is being used) or to access the repo (if a private 'source-repo' is being used)
2323
required: false
2424

2525
delete-other-labels:

lib/index.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

+91-16
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,28 @@ const log = {
1717
core[showInReport ? 'error' : 'info']('✗ ' + str),
1818
fatal: (str: string) => core.setFailed('✗ ' + str)
1919
}
20-
let usingLocalFile!: boolean
20+
21+
type ConfigSource = 'local' | 'remote' | 'repo'
22+
let configSource!: ConfigSource
2123
;(async () => {
2224
try {
2325
checkInputs()
2426

25-
const labels = usingLocalFile
26-
? readConfigFile(getInput('config-file'))
27-
: await fetchRepoLabels(
27+
let labels: LabelInfo[]
28+
switch (configSource) {
29+
case 'local':
30+
labels = readConfigFile(getInput('config-file'))
31+
break
32+
case 'remote':
33+
labels = await readRemoteConfigFile(getInput('config-file'))
34+
break
35+
case 'repo':
36+
labels = await fetchRepoLabels(
2837
getInput('source-repo'),
29-
getInput('source-repo-token')
38+
getInput('request-token')
3039
)
40+
break
41+
}
3142

3243
startGroup('Syncing labels...')
3344
const options: Options = {
@@ -95,19 +106,38 @@ function readConfigFile(filePath: string) {
95106
try {
96107
// Read the file from the given path
97108
log.info('Reading file...')
98-
file = fs.readFileSync(path.resolve(filePath), { encoding: 'utf-8' })
109+
110+
const resolvedPath = path.resolve(filePath)
111+
core.debug(`Resolved path: ${resolvedPath}`)
112+
113+
file = fs.readFileSync(resolvedPath, { encoding: 'utf-8' })
114+
core.debug(`fs ok: type ${typeof file}`)
115+
core.debug(file)
116+
99117
if (!file || typeof file != 'string') throw null
100-
} catch {
118+
} catch (e) {
119+
core.debug(`Actual error: ${e}`)
101120
throw "Can't access config file."
102121
}
103122

123+
const parsed = parseConfigFile(path.extname(filePath).toLowerCase(), file)
124+
125+
log.success('File parsed successfully.')
126+
log.info('Parsed config:\n' + JSON.stringify(parsed, null, 2))
127+
endGroup()
128+
return parsed
129+
}
130+
131+
function parseConfigFile(
132+
fileExtension: string,
133+
unparsedConfig: string
134+
): LabelInfo[] {
104135
let parsed: LabelInfo[]
105-
const fileExtension = path.extname(filePath).toLowerCase()
106136

107137
if (['.yaml', '.yml'].includes(fileExtension)) {
108138
// Parse YAML file
109139
log.info('Parsing YAML file...')
110-
parsed = yaml.parse(file)
140+
parsed = yaml.parse(unparsedConfig)
111141
try {
112142
throwConfigError(parsed)
113143
} catch (e) {
@@ -118,7 +148,7 @@ function readConfigFile(filePath: string) {
118148
// Try to parse JSON file
119149
log.info('Parsing JSON file...')
120150
try {
121-
parsed = JSON.parse(file)
151+
parsed = JSON.parse(unparsedConfig)
122152
} catch {
123153
throw "Couldn't parse JSON config file, check for syntax errors."
124154
}
@@ -133,7 +163,37 @@ function readConfigFile(filePath: string) {
133163
throw `Invalid file extension: ${fileExtension}`
134164
}
135165

136-
log.success('File parsed successfully.')
166+
return parsed
167+
}
168+
169+
async function readRemoteConfigFile(fileURL: string): Promise<LabelInfo[]> {
170+
startGroup('Reading remote config file...')
171+
const token = getInput('request-token')
172+
173+
const headers = token
174+
? {
175+
Authorization: `token ${token}`
176+
}
177+
: undefined
178+
log.info(`Using following URL: ${fileURL}`)
179+
180+
const { data } = await axios.get(fileURL, { headers })
181+
if (!data || typeof data !== 'string')
182+
throw "Can't get remote config file from GitHub API"
183+
184+
log.success(`Remote file config fetched correctly.`)
185+
186+
const parsed = parseConfigFile(path.extname(fileURL).toLowerCase(), data)
187+
188+
log.success('Remote file parsed successfully.')
189+
190+
try {
191+
throwConfigError(parsed)
192+
} catch (e) {
193+
log.error(JSON.stringify(parsed, null, 2), false)
194+
throw 'Parsed JSON file is invalid:\n' + e
195+
}
196+
137197
log.info('Parsed config:\n' + JSON.stringify(parsed, null, 2))
138198
endGroup()
139199
return parsed
@@ -168,21 +228,23 @@ function checkInputs() {
168228
let cb = () => {}
169229

170230
startGroup('Checking inputs...')
171-
log.info('Checking inputs...')
172231
if (!getInput('token')) throw 'The token parameter is required.'
173232

174233
const configFile = getInput('config-file'),
175234
sourceRepo = getInput('source-repo')
176235

177-
if (!!configFile == !!sourceRepo)
236+
if (!!configFile && !!sourceRepo)
178237
throw "You can't use a config file and a source repo at the same time. Choose one!"
179238

180-
// config-file: doesn't need evaluation, will be evaluated when parsing
181-
usingLocalFile = !!configFile
239+
if (configFile) configSource = isURL(configFile) ? 'remote' : 'local'
240+
else if (sourceRepo) configSource = 'repo'
241+
else throw 'You have to either use a config file or a source repo.'
242+
243+
log.info(`Current config mode: ${configSource}`)
182244

183245
if (sourceRepo && sourceRepo.split('/').length != 2)
184246
throw 'Source repo should be in the owner/repo format, like EndBug/label-sync!'
185-
if (sourceRepo && !getInput('source-repo-token'))
247+
if (sourceRepo && !getInput('request-token'))
186248
cb = () =>
187249
log.warning(
188250
"You're using a source repo without a token: if your repository is private the action won't be able to read the labels.",
@@ -199,3 +261,16 @@ function checkInputs() {
199261

200262
cb()
201263
}
264+
265+
function isURL(str: string) {
266+
const pattern = new RegExp(
267+
'^(https?:\\/\\/)?' + // protocol
268+
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
269+
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
270+
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
271+
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
272+
'(\\#[-a-z\\d_]*)?$',
273+
'i'
274+
) // fragment locator
275+
return !!pattern.test(str)
276+
}

0 commit comments

Comments
 (0)