feat: add pathspec_error_handling input (#280)

* feat: add pathspec_error_handling input

* fix: show add/rm errors on same line

* docs(README): add docs for new input
This commit is contained in:
Federico Grandi
2021-09-06 16:30:10 +02:00
parent 68050c9e64
commit 4d9c6e96c4
5 changed files with 149 additions and 83 deletions

View File

@@ -54,6 +54,13 @@ Add a step like this to your workflow:
# Default: 'Commit from GitHub Actions (name of the workflow)' # Default: 'Commit from GitHub Actions (name of the workflow)'
message: 'Your commit message' message: 'Your commit message'
# The way the action should handle pathspec errors from the add and remove commands. Three options are available:
# - ignore -> errors will be logged but the step won't fail
# - exitImmediately -> the action will stop right away, and the step will fail
# - exitAtEnd -> the action will go on, every pathspec error will be logged at the end, the step will fail.
# Default: ignore
pathspec_error_handling: ignore
# The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all. # The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all.
# Default: '--no-rebase' # Default: '--no-rebase'
pull_strategy: 'NO-PULL or --no-rebase or --no-ff or --rebase' pull_strategy: 'NO-PULL or --no-rebase or --no-ff or --rebase'

View File

@@ -32,6 +32,10 @@ inputs:
message: message:
description: The message for the commit description: The message for the commit
required: false required: false
pathspec_error_handling:
description: The way the action should handle pathspec errors from the add and remove commands.
required: false
default: ignore
pull_strategy: pull_strategy:
description: The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all. description: The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all.
required: false required: false

6
lib/index.js generated

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ import YAML from 'js-yaml'
import { import {
getInput, getInput,
getUserInfo, getUserInfo,
Input, input,
log, log,
matchGitArgs, matchGitArgs,
outputs, outputs,
@@ -15,6 +15,9 @@ import {
const baseDir = path.join(process.cwd(), getInput('cwd') || '') const baseDir = path.join(process.cwd(), getInput('cwd') || '')
const git = simpleGit({ baseDir }) const git = simpleGit({ baseDir })
const exitErrors: Error[] = []
core.info(`Running in ${baseDir}`) core.info(`Running in ${baseDir}`)
;(async () => { ;(async () => {
await checkInputs().catch(core.setFailed) await checkInputs().catch(core.setFailed)
@@ -22,14 +25,15 @@ core.info(`Running in ${baseDir}`)
core.startGroup('Internal logs') core.startGroup('Internal logs')
core.info('> Staging files...') core.info('> Staging files...')
const peh = getInput('pathspec_error_handling')
if (getInput('add')) { if (getInput('add')) {
core.info('> Adding files...') core.info('> Adding files...')
await add() await add(peh == 'ignore' ? 'pathspec' : 'none')
} else core.info('> No files to add.') } else core.info('> No files to add.')
if (getInput('remove')) { if (getInput('remove')) {
core.info('> Removing files...') core.info('> Removing files...')
await remove() await remove(peh == 'ignore' ? 'pathspec' : 'none')
} else core.info('> No files to remove.') } else core.info('> No files to remove.')
core.info('> Checking for uncommitted changes in the git working tree...') core.info('> Checking for uncommitted changes in the git working tree...')
@@ -71,8 +75,8 @@ core.info(`Running in ${baseDir}`)
} }
core.info('> Re-staging files...') core.info('> Re-staging files...')
if (getInput('add')) await add({ ignoreErrors: true }) if (getInput('add')) await add('all')
if (getInput('remove')) await remove({ ignoreErrors: true }) if (getInput('remove')) await remove('all')
core.info('> Creating commit...') core.info('> Creating commit...')
await git.commit( await git.commit(
@@ -97,7 +101,7 @@ core.info(`Running in ${baseDir}`)
if (getInput('tag')) { if (getInput('tag')) {
core.info('> Tagging commit...') core.info('> Tagging commit...')
await git await git
.tag(matchGitArgs(getInput('tag')), (err, data?) => { .tag(matchGitArgs(getInput('tag') || ''), (err, data?) => {
if (data) setOutput('tagged', 'true') if (data) setOutput('tagged', 'true')
return log(err, data) return log(err, data)
}) })
@@ -159,7 +163,7 @@ core.info(`Running in ${baseDir}`)
{ {
'--delete': null, '--delete': null,
origin: null, origin: null,
[matchGitArgs(getInput('tag')).filter( [matchGitArgs(getInput('tag') || '').filter(
(w) => !w.startsWith('-') (w) => !w.startsWith('-')
)[0]]: null )[0]]: null
}, },
@@ -177,6 +181,14 @@ core.info(`Running in ${baseDir}`)
core.info('> Working tree clean. Nothing to commit.') core.info('> Working tree clean. Nothing to commit.')
} }
})() })()
.then(() => {
// Check for exit errors
if (exitErrors.length == 1) throw exitErrors[0]
else if (exitErrors.length > 1) {
exitErrors.forEach((e) => core.error(e))
throw 'There have been multiple runtime errors.'
}
})
.then(logOutputs) .then(logOutputs)
.catch((e) => { .catch((e) => {
core.endGroup() core.endGroup()
@@ -185,11 +197,11 @@ core.info(`Running in ${baseDir}`)
}) })
async function checkInputs() { async function checkInputs() {
function setInput(input: Input, value: string | undefined) { function setInput(input: input, value: string | undefined) {
if (value) return (process.env[`INPUT_${input.toUpperCase()}`] = value) if (value) return (process.env[`INPUT_${input.toUpperCase()}`] = value)
else return delete process.env[`INPUT_${input.toUpperCase()}`] else return delete process.env[`INPUT_${input.toUpperCase()}`]
} }
function setDefault(input: Input, value: string) { function setDefault(input: input, value: string) {
if (!getInput(input)) setInput(input, value) if (!getInput(input)) setInput(input, value)
return getInput(input) return getInput(input)
} }
@@ -219,7 +231,7 @@ async function checkInputs() {
else core.setFailed('Add input: array length < 1') else core.setFailed('Add input: array length < 1')
} }
if (getInput('remove')) { if (getInput('remove')) {
const parsed = parseInputArray(getInput('remove')) const parsed = parseInputArray(getInput('remove') || '')
if (parsed.length == 1) if (parsed.length == 1)
core.info( core.info(
'Remove input parsed as single string, running 1 git rm command.' 'Remove input parsed as single string, running 1 git rm command.'
@@ -327,25 +339,16 @@ async function checkInputs() {
core.info(`> Running for a PR, the action will use '${branch}' as ref.`) core.info(`> Running for a PR, the action will use '${branch}' as ref.`)
// #endregion // #endregion
// #region signoff // #region pathspec_error_handling
if (getInput('signoff')) { const peh_valid = ['ignore', 'exitImmediately', 'exitAtEnd']
const parsed = getInput('signoff', true) if (!peh_valid.includes(getInput('pathspec_error_handling')))
throw new Error(
if (parsed === undefined) `"${getInput(
throw new Error( 'pathspec_error_handling'
`"${getInput( )}" is not a valid value for the 'pathspec_error_handling' input. Valid values are: ${peh_valid.join(
'signoff' ', '
)}" is not a valid value for the 'signoff' input: only "true" and "false" are allowed.` )}`
)
if (!parsed) setInput('signoff', undefined)
core.debug(
`Current signoff option: ${getInput('signoff')} (${typeof getInput(
'signoff'
)})`
) )
}
// #endregion // #endregion
// #region pull_strategy // #region pull_strategy
@@ -368,6 +371,27 @@ async function checkInputs() {
} }
// #endregion // #endregion
// #region signoff
if (getInput('signoff')) {
const parsed = getInput('signoff', true)
if (parsed === undefined)
throw new Error(
`"${getInput(
'signoff'
)}" is not a valid value for the 'signoff' input: only "true" and "false" are allowed.`
)
if (!parsed) setInput('signoff', undefined)
core.debug(
`Current signoff option: ${getInput('signoff')} (${typeof getInput(
'signoff'
)})`
)
}
// #endregion
// #region github_token // #region github_token
if (!getInput('github_token')) if (!getInput('github_token'))
core.warning( core.warning(
@@ -376,9 +400,9 @@ async function checkInputs() {
// #endregion // #endregion
} }
async function add({ logWarning = true, ignoreErrors = false } = {}): Promise< async function add(
(void | Response<void>)[] ignoreErrors: 'all' | 'pathspec' | 'none' = 'none'
> { ): Promise<(void | Response<void>)[]> {
const input = getInput('add') const input = getInput('add')
if (!input) return [] if (!input) return []
@@ -391,19 +415,26 @@ async function add({ logWarning = true, ignoreErrors = false } = {}): Promise<
// If any of them fails, the whole function will return a Promise rejection // If any of them fails, the whole function will return a Promise rejection
await git await git
.add(matchGitArgs(args), (err: any, data?: any) => .add(matchGitArgs(args), (err: any, data?: any) =>
log(ignoreErrors ? null : err, data) log(ignoreErrors == 'all' ? null : err, data)
) )
.catch((e: Error) => { .catch((e: Error) => {
if (ignoreErrors) return // if I should ignore every error, return
if (ignoreErrors == 'all') return
// if it's a pathspec error...
if ( if (
e.message.includes('fatal: pathspec') && e.message.includes('fatal: pathspec') &&
e.message.includes('did not match any files') && e.message.includes('did not match any files')
logWarning ) {
) if (ignoreErrors == 'pathspec') return
core.warning(
`Add command did not match any file:\n git add ${args}` const peh = getInput('pathspec_error_handling'),
) err = new Error(
else throw e `Add command did not match any file: git add ${args}`
)
if (peh == 'exitImmediately') throw err
if (peh == 'exitAtEnd') exitErrors.push(err)
} else throw e
}) })
) )
} }
@@ -411,10 +442,9 @@ async function add({ logWarning = true, ignoreErrors = false } = {}): Promise<
return res return res
} }
async function remove({ async function remove(
logWarning = true, ignoreErrors: 'all' | 'pathspec' | 'none' = 'none'
ignoreErrors = false ): Promise<(void | Response<void>)[]> {
} = {}): Promise<(void | Response<void>)[]> {
const input = getInput('remove') const input = getInput('remove')
if (!input) return [] if (!input) return []
@@ -427,19 +457,26 @@ async function remove({
// If any of them fails, the whole function will return a Promise rejection // If any of them fails, the whole function will return a Promise rejection
await git await git
.rm(matchGitArgs(args), (e: any, d?: any) => .rm(matchGitArgs(args), (e: any, d?: any) =>
log(ignoreErrors ? null : e, d) log(ignoreErrors == 'all' ? null : e, d)
) )
.catch((e: Error) => { .catch((e: Error) => {
if (ignoreErrors) return // if I should ignore every error, return
if (ignoreErrors == 'all') return
// if it's a pathspec error...
if ( if (
e.message.includes('fatal: pathspec') && e.message.includes('fatal: pathspec') &&
e.message.includes('did not match any files') e.message.includes('did not match any files')
) ) {
logWarning && if (ignoreErrors == 'pathspec') return
core.warning(
const peh = getInput('pathspec_error_handling'),
err = new Error(
`Remove command did not match any file:\n git rm ${args}` `Remove command did not match any file:\n git rm ${args}`
) )
else throw e if (peh == 'exitImmediately') throw err
if (peh == 'exitAtEnd') exitErrors.push(err)
} else throw e
}) })
) )
} }

View File

@@ -3,27 +3,44 @@ import * as core from '@actions/core'
import { Toolkit } from 'actions-toolkit' import { Toolkit } from 'actions-toolkit'
import fs from 'fs' import fs from 'fs'
export type Input = interface InputTypes {
| 'add' add: string
| 'author_name' author_name: string
| 'author_email' author_email: string
| 'branch' branch: string
| 'committer_name' committer_name: string
| 'committer_email' committer_email: string
| 'cwd' cwd: string
| 'default_author' default_author: 'github_actor' | 'user_info' | 'github_actions'
| 'message' message: string
| 'pull_strategy' pathspec_error_handling: 'ignore' | 'exitImmediately' | 'exitAtEnd'
| 'push' pull_strategy: string
| 'remove' push: string
| 'signoff' remove: string | undefined
| 'tag' signoff: undefined
| 'github_token' tag: string | undefined
export type Output = 'committed' | 'commit_sha' | 'pushed' | 'tagged' github_token: string | undefined
}
export type input = keyof InputTypes
interface OutputTypes {
committed: 'true' | 'false'
commit_sha: string | undefined
pushed: 'true' | 'false'
tagged: 'true' | 'false'
}
export type output = keyof OutputTypes
export const outputs: OutputTypes = {
committed: 'false',
commit_sha: undefined,
pushed: 'false',
tagged: 'false'
}
type RecordOf<T extends string> = Record<T, string | undefined> type RecordOf<T extends string> = Record<T, string | undefined>
export const tools = new Toolkit<RecordOf<Input>, RecordOf<Output>>({ export const tools = new Toolkit<RecordOf<input>, RecordOf<output>>({
secrets: [ secrets: [
'GITHUB_EVENT_PATH', 'GITHUB_EVENT_PATH',
'GITHUB_EVENT_NAME', 'GITHUB_EVENT_NAME',
@@ -31,18 +48,19 @@ export const tools = new Toolkit<RecordOf<Input>, RecordOf<Output>>({
'GITHUB_ACTOR' 'GITHUB_ACTOR'
] ]
}) })
export const outputs: Record<Output, any> = {
committed: 'false',
commit_sha: undefined,
pushed: 'false',
tagged: 'false'
}
export function getInput(name: Input, bool: true): boolean export function getInput<T extends input>(name: T, parseAsBool: true): boolean
export function getInput(name: Input, bool?: false): string export function getInput<T extends input>(
export function getInput(name: Input, bool = false) { name: T,
if (bool) return core.getBooleanInput(name) parseAsBool?: false
return tools.inputs[name] || '' ): InputTypes[T]
export function getInput<T extends input>(
name: T,
parseAsBool = false
): InputTypes[T] | boolean {
if (parseAsBool) return core.getBooleanInput(name)
// @ts-expect-error
return core.getInput(name)
} }
export async function getUserInfo(username?: string) { export async function getUserInfo(username?: string) {
@@ -113,7 +131,7 @@ export function readJSON(filePath: string) {
} }
} }
export function setOutput<T extends Output>(name: T, value: typeof outputs[T]) { export function setOutput<T extends output>(name: T, value: OutputTypes[T]) {
core.debug(`Setting output: ${name}=${value}`) core.debug(`Setting output: ${name}=${value}`)
outputs[name] = value outputs[name] = value
core.setOutput(name, value) core.setOutput(name, value)