Skip to content

Commit

Permalink
feat!: add option to use remote config files (#2)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
h1dden-da3m0n and EndBug authored Jul 2, 2021
1 parent baf556b commit 9d317a2
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 35 deletions.
50 changes: 41 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ jobs:
runs-on: ubuntu-latest

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

# If you want to use a source repo, you can put is name here (only the owner/repo format is accepted)
source-repo: owner/repo
# If you're using a private source repo, you'll need to add a custom token for the action to read it
source-repo-token: ${{ secrets.YOUR_OWN_SECRET }}

# 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
request-token: ${{ secrets.YOUR_OWN_SECRET }}

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

```yaml
- name: A label
color: "000000"
color: '000000'
- name: Another label
color: "111111"
color: '111111'
description: A very inspiring description
- name: Yet another label
color: "222222"
aliases: ["first", "second", "third"]
color: '222222'
aliases: ['first', 'second', 'third']
```

```json
Expand All @@ -88,3 +91,32 @@ This is how it would end up looking:
```

If you want to see an actual config file, you can check out the one in this repo [here](.github/labels.yml).

This action can either read a local file or fetch it from a custom URL.
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.

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.
You can use the "raw" link that GitHub provides for the file:

```yaml
- uses: EndBug/label-sync@v2
with:
# This is just an example, but any valid URL can be used
config-file: 'https://raw.githubusercontent.com/EndBug/label-sync/main/.github/labels.yml'
```

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.

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:

```yaml
- uses: EndBug/label-sync@v2
with:
config-file: 'https://raw.githubusercontent.com/User/repo-name/path/to/labels.yml'
# Remember not to put PATs in files, use GitHub secrets instead
request-token: ${{ secrets.YOUR_CUSTOM_PAT }}
```

The `request-token` input can also be used with a `source-repo`, if that repo is private.

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.
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ inputs:
default: ${{ github.token }}

config-file:
description: The path to the JSON or YAML file containing the label config (more info in the README)
description: The path (or URL) to the JSON or YAML file containing the label config (more info in the README)
required: false
source-repo:
description: The repo to copy labels from (if not using a config file), in the 'owner/repo' format
required: false
source-repo-token:
description: A token to use if the source repo is private
request-token:
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)
required: false

delete-other-labels:
Expand Down
2 changes: 1 addition & 1 deletion lib/index.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 91 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,28 @@ const log = {
core[showInReport ? 'error' : 'info']('✗ ' + str),
fatal: (str: string) => core.setFailed('✗ ' + str)
}
let usingLocalFile!: boolean

type ConfigSource = 'local' | 'remote' | 'repo'
let configSource!: ConfigSource
;(async () => {
try {
checkInputs()

const labels = usingLocalFile
? readConfigFile(getInput('config-file'))
: await fetchRepoLabels(
let labels: LabelInfo[]
switch (configSource) {
case 'local':
labels = readConfigFile(getInput('config-file'))
break
case 'remote':
labels = await readRemoteConfigFile(getInput('config-file'))
break
case 'repo':
labels = await fetchRepoLabels(
getInput('source-repo'),
getInput('source-repo-token')
getInput('request-token')
)
break
}

startGroup('Syncing labels...')
const options: Options = {
Expand Down Expand Up @@ -95,19 +106,38 @@ function readConfigFile(filePath: string) {
try {
// Read the file from the given path
log.info('Reading file...')
file = fs.readFileSync(path.resolve(filePath), { encoding: 'utf-8' })

const resolvedPath = path.resolve(filePath)
core.debug(`Resolved path: ${resolvedPath}`)

file = fs.readFileSync(resolvedPath, { encoding: 'utf-8' })
core.debug(`fs ok: type ${typeof file}`)
core.debug(file)

if (!file || typeof file != 'string') throw null
} catch {
} catch (e) {
core.debug(`Actual error: ${e}`)
throw "Can't access config file."
}

const parsed = parseConfigFile(path.extname(filePath).toLowerCase(), file)

log.success('File parsed successfully.')
log.info('Parsed config:\n' + JSON.stringify(parsed, null, 2))
endGroup()
return parsed
}

function parseConfigFile(
fileExtension: string,
unparsedConfig: string
): LabelInfo[] {
let parsed: LabelInfo[]
const fileExtension = path.extname(filePath).toLowerCase()

if (['.yaml', '.yml'].includes(fileExtension)) {
// Parse YAML file
log.info('Parsing YAML file...')
parsed = yaml.parse(file)
parsed = yaml.parse(unparsedConfig)
try {
throwConfigError(parsed)
} catch (e) {
Expand All @@ -118,7 +148,7 @@ function readConfigFile(filePath: string) {
// Try to parse JSON file
log.info('Parsing JSON file...')
try {
parsed = JSON.parse(file)
parsed = JSON.parse(unparsedConfig)
} catch {
throw "Couldn't parse JSON config file, check for syntax errors."
}
Expand All @@ -133,7 +163,37 @@ function readConfigFile(filePath: string) {
throw `Invalid file extension: ${fileExtension}`
}

log.success('File parsed successfully.')
return parsed
}

async function readRemoteConfigFile(fileURL: string): Promise<LabelInfo[]> {
startGroup('Reading remote config file...')
const token = getInput('request-token')

const headers = token
? {
Authorization: `token ${token}`
}
: undefined
log.info(`Using following URL: ${fileURL}`)

const { data } = await axios.get(fileURL, { headers })
if (!data || typeof data !== 'string')
throw "Can't get remote config file from GitHub API"

log.success(`Remote file config fetched correctly.`)

const parsed = parseConfigFile(path.extname(fileURL).toLowerCase(), data)

log.success('Remote file parsed successfully.')

try {
throwConfigError(parsed)
} catch (e) {
log.error(JSON.stringify(parsed, null, 2), false)
throw 'Parsed JSON file is invalid:\n' + e
}

log.info('Parsed config:\n' + JSON.stringify(parsed, null, 2))
endGroup()
return parsed
Expand Down Expand Up @@ -168,21 +228,23 @@ function checkInputs() {
let cb = () => {}

startGroup('Checking inputs...')
log.info('Checking inputs...')
if (!getInput('token')) throw 'The token parameter is required.'

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

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

// config-file: doesn't need evaluation, will be evaluated when parsing
usingLocalFile = !!configFile
if (configFile) configSource = isURL(configFile) ? 'remote' : 'local'
else if (sourceRepo) configSource = 'repo'
else throw 'You have to either use a config file or a source repo.'

log.info(`Current config mode: ${configSource}`)

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

cb()
}

function isURL(str: string) {
const pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i'
) // fragment locator
return !!pattern.test(str)
}

0 comments on commit 9d317a2

Please sign in to comment.