Source: github.js

import * as TYPES from '#types/types.js' // eslint-disable-line
import { request } from '@octokit/request'
import chalk from 'chalk'
import ora from 'ora'
import { splitLogToGenericAndBreaking, filterGenericCommits } from '#utils/parse.js'

/**
* @ignore
* @typedef {TYPES.DATA} DATA {@link DATA}
* @typedef {TYPES.COMMIT_MESSAGE} COMMIT_MESSAGE {@link COMMIT_MESSAGE}
*/

/**
 * #### check if GitHub token is set and valid to use
 * @async
 * @memberof GITHUB
 * @param {DATA} data - data object
 * @returns undefined
 */
async function checkGithubToken (data) {
  const spinner = ora('Checking GitHub token').start()
  if (data.env.GITHUB_TOKEN) {
    try {
      await request('GET /repos/{owner}/{repo}/commits?sha={branch}&per_page={per_page}', {
        headers: {
          authorization: `token ${data.env.GITHUB_TOKEN}`
        },
        owner: data.git.remote.owner,
        repo: data.git.remote.name,
        branch: data.git.branch,
        per_page: 1
      })
      data.github.token = true
      spinner.succeed()
    } catch (error) {
      spinner.fail()
      console.warn(`${chalk.red('Warning:')} GitHub token is invalid. Github release generation will be skipped.`)
      if (process.env.QR_MODE === 'debug') {
        console.warn(error)
      }
    }
  } else {
    spinner.fail()
    console.warn(`${chalk.red('Warning:')} GitHub token is not set. Github release generation will be skipped.`)
    console.warn(`Add ${chalk.yellow('GITHUB_TOKEN=your-token')} to ${chalk.green('.env')} file to enable GitHub release generation.`)
  }
}

/**
 * #### get github commits from last tag date to HEAD
 * @async
 * @private
 * @memberof GITHUB
 * @param {DATA} data - data object
 * @returns {Promise<COMMIT_MESSAGE[]|undefined>} - undefined
 */
async function getGithubCommits (data) {
  const spinner = ora('Get GitHub commits').start()
  try {
    const commits = await request('GET /repos/{owner}/{repo}/commits?sha={branch}&since={date}&per_page={per_page}', {
      headers: {
        authorization: `token ${data.env.GITHUB_TOKEN}`
      },
      owner: data.git.remote.owner,
      repo: data.git.remote.name,
      branch: data.git.branch,
      date: data.git.log[data.git.log.length - 1].date,
      per_page: 100
    })
    const parsedCommits = parseGithubCommits(data, commits.data)
    spinner.succeed()
    return parsedCommits
  } catch (error) {
    spinner.fail()
    console.error(`${chalk.bold.red('Error:')} GitHub commits are not received. Github release generation will be skipped.`)
    if (process.env.QR_MODE === 'debug') {
      console.error(error)
    }
  }
}

/**
 * #### parse github API commits
 * @private
 * @memberof GITHUB
 * @param {DATA} data - data object
 * @param {Array<Object>} commits - github commits
 * @returns {Array<COMMIT_MESSAGE>} - parsed commits
 */
function parseGithubCommits (data, commits) {
  /** @type {Array<COMMIT_MESSAGE>} */
  const parsedCommits = []
  commits.forEach((/** @type {any} */ item) => {
    const prefix = ''
    const message = item.commit.message.split('\n')[0]
    const hash = item.sha.slice(0, 7)
    const name = item.author.login
    parsedCommits.push({ prefix, message, name, hash })
  })
  return parsedCommits
}

/**
 * #### save GitHub commits
 * @async
 * @memberof GITHUB
 * @param {DATA} data - data object
 * @returns undefined
 */
async function saveGithubCommits (data) {
  data.github.commits.all = await getGithubCommits(data) ?? []
  if (data.github.commits.all.length > 0) {
    data.github.commits = { ...data.github.commits, ...splitLogToGenericAndBreaking(data.github.commits.all, data.config.changelog.breakingLabel) }
    data.github.commits.generic = filterGenericCommits(data.github.commits.generic, data.config.changelog.labels)
  }
}

/**
 * #### format github release Notes
 * @memberof GITHUB
 * @param {DATA} data - data object
 * @returns undefined
 */
function saveGithubReleaseNotes (data) {
  let generic = ''
  let breaking = ''
  const compareLink = `\n***\n**Full Changelog**: https://${data.git.remote.host}/${data.git.remote.full_name}/compare/${data.git.lastTag}...${data.answers.releaseVersion}\n`
  const changelogLink = `[${data.config.changelog.file}](https://${data.git.remote.host}/${data.git.remote.full_name}/blob/${data.git.branch}/${data.config.changelog.file})\n`
  const changelogLabels = data.config.changelog.labels
  const githubReleaseTitles = data.config.githubReleaseTitles
  const githubCommits = data.github.commits.generic
  const githubCommitsBreaking = data.github.commits.breaking
  if (githubCommits.length > 0 && changelogLabels && changelogLabels.length > 0) {
    if (githubReleaseTitles && changelogLabels.length === githubReleaseTitles.length) {
      /** @type {any} */
      const releaseNotesData = []
      // regroup commits by labels
      githubReleaseTitles.forEach((title, index) => {
        releaseNotesData.push(githubCommits.filter((log) => {
          return log.prefix.includes(changelogLabels[index])
        }))
      })
      releaseNotesData.forEach((/** @type {Array<COMMIT_MESSAGE>} */ item, /** @type {number} */ index) => {
        if (item.length > 0) {
          generic += `### ${githubReleaseTitles[index]}\n`
          item.forEach((log) => {
            log.message = log.message.charAt(0).toUpperCase() + log.message.slice(1)
            generic += `* ${log.message} @${log.name} ${log.hash}\n`
          })
        }
      })
    } else {
      changelogLabels.forEach((label) => {
        githubCommits.forEach((item) => {
          if (item.prefix === label) {
            item.message = item.message.charAt(0).toUpperCase() + item.message.slice(1)
            generic += `* **${item.prefix}** ${item.message} @${item.name} ${item.hash}\n`
          }
        })
      })
    }
  } else {
    githubCommits.shift()
    githubCommits.forEach((item, index) => {
      item.message = item.message.charAt(0).toUpperCase() + item.message.slice(1)
      generic += `* ${item.message} @${item.name} ${item.hash}\n`
    })
  }
  if (githubCommitsBreaking.length > 0) {
    breaking = '### Breaking Changes\n'
    githubCommitsBreaking.forEach((/** @type {any} */ item) => {
      item.message = item.message.charAt(0).toUpperCase() + item.message.slice(1)
      breaking += `* ${item.message} @${item.name} ${item.hash}\n`
    })
  }
  data.github.releaseNotes = breaking + generic + compareLink + changelogLink
}

/**
 * #### publish GitHub release
 * @async
 * @memberof GITHUB
 * @param {DATA} data - data object
 * @returns undefined
 */
async function publishGithubRelease (data) {
  const spinner = ora('Publish GitHub release').start()
  try {
    await request('POST /repos/{owner}/{repo}/releases', {
      headers: {
        authorization: `token ${data.env.GITHUB_TOKEN}`
      },
      owner: data.git.remote.owner ?? '',
      repo: data.git.remote.name ?? '',
      tag_name: data.answers.releaseVersion ?? '',
      target_commitish: data.git.branch,
      name: data.answers.releaseVersion,
      body: data.github.releaseNotes,
      draft: false,
      prerelease: false,
      generate_release_notes: false
    })
    spinner.succeed()
  } catch (error) {
    spinner.fail()
    console.error(`${chalk.bold.red('Error:')} Publish GitHub release failed.`)
    if (process.env.QR_MODE === 'debug') {
      console.error(error)
    }
  }
}

export { checkGithubToken, saveGithubCommits, saveGithubReleaseNotes, publishGithubRelease }

Table of contents