|
| 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