Skip to content

feat: add pathspec_error_handling input #280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ Add a step like this to your workflow:
# Default: 'Commit from GitHub Actions (name of the workflow)'
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.
# Default: '--no-rebase'
pull_strategy: 'NO-PULL or --no-rebase or --no-ff or --rebase'
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ inputs:
message:
description: The message for the commit
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:
description: The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all.
required: false
Expand Down
6 changes: 3 additions & 3 deletions lib/index.js

Large diffs are not rendered by default.

137 changes: 87 additions & 50 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import YAML from 'js-yaml'
import {
getInput,
getUserInfo,
Input,
input,
log,
matchGitArgs,
outputs,
Expand All @@ -15,21 +15,25 @@ import {

const baseDir = path.join(process.cwd(), getInput('cwd') || '')
const git = simpleGit({ baseDir })

const exitErrors: Error[] = []

core.info(`Running in ${baseDir}`)
;(async () => {
await checkInputs().catch(core.setFailed)

core.startGroup('Internal logs')
core.info('> Staging files...')

const peh = getInput('pathspec_error_handling')
if (getInput('add')) {
core.info('> Adding files...')
await add()
await add(peh == 'ignore' ? 'pathspec' : 'none')
} else core.info('> No files to add.')

if (getInput('remove')) {
core.info('> Removing files...')
await remove()
await remove(peh == 'ignore' ? 'pathspec' : 'none')
} else core.info('> No files to remove.')

core.info('> Checking for uncommitted changes in the git working tree...')
Expand Down Expand Up @@ -71,8 +75,8 @@ core.info(`Running in ${baseDir}`)
}

core.info('> Re-staging files...')
if (getInput('add')) await add({ ignoreErrors: true })
if (getInput('remove')) await remove({ ignoreErrors: true })
if (getInput('add')) await add('all')
if (getInput('remove')) await remove('all')

core.info('> Creating commit...')
await git.commit(
Expand All @@ -97,7 +101,7 @@ core.info(`Running in ${baseDir}`)
if (getInput('tag')) {
core.info('> Tagging commit...')
await git
.tag(matchGitArgs(getInput('tag')), (err, data?) => {
.tag(matchGitArgs(getInput('tag') || ''), (err, data?) => {
if (data) setOutput('tagged', 'true')
return log(err, data)
})
Expand Down Expand Up @@ -159,7 +163,7 @@ core.info(`Running in ${baseDir}`)
{
'--delete': null,
origin: null,
[matchGitArgs(getInput('tag')).filter(
[matchGitArgs(getInput('tag') || '').filter(
(w) => !w.startsWith('-')
)[0]]: null
},
Expand All @@ -177,6 +181,14 @@ core.info(`Running in ${baseDir}`)
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)
.catch((e) => {
core.endGroup()
Expand All @@ -185,11 +197,11 @@ core.info(`Running in ${baseDir}`)
})

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)
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)
return getInput(input)
}
Expand Down Expand Up @@ -219,7 +231,7 @@ async function checkInputs() {
else core.setFailed('Add input: array length < 1')
}
if (getInput('remove')) {
const parsed = parseInputArray(getInput('remove'))
const parsed = parseInputArray(getInput('remove') || '')
if (parsed.length == 1)
core.info(
'Remove input parsed as single string, running 1 git rm command.'
Expand Down Expand Up @@ -327,25 +339,16 @@ async function checkInputs() {
core.info(`> Running for a PR, the action will use '${branch}' as ref.`)
// #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'
)})`
// #region pathspec_error_handling
const peh_valid = ['ignore', 'exitImmediately', 'exitAtEnd']
if (!peh_valid.includes(getInput('pathspec_error_handling')))
throw new Error(
`"${getInput(
'pathspec_error_handling'
)}" is not a valid value for the 'pathspec_error_handling' input. Valid values are: ${peh_valid.join(
', '
)}`
)
}
// #endregion

// #region pull_strategy
Expand All @@ -368,6 +371,27 @@ async function checkInputs() {
}
// #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
if (!getInput('github_token'))
core.warning(
Expand All @@ -376,9 +400,9 @@ async function checkInputs() {
// #endregion
}

async function add({ logWarning = true, ignoreErrors = false } = {}): Promise<
(void | Response<void>)[]
> {
async function add(
ignoreErrors: 'all' | 'pathspec' | 'none' = 'none'
): Promise<(void | Response<void>)[]> {
const input = getInput('add')
if (!input) return []

Expand All @@ -391,30 +415,36 @@ async function add({ logWarning = true, ignoreErrors = false } = {}): Promise<
// If any of them fails, the whole function will return a Promise rejection
await git
.add(matchGitArgs(args), (err: any, data?: any) =>
log(ignoreErrors ? null : err, data)
log(ignoreErrors == 'all' ? null : err, data)
)
.catch((e: Error) => {
if (ignoreErrors) return
// if I should ignore every error, return
if (ignoreErrors == 'all') return

// if it's a pathspec error...
if (
e.message.includes('fatal: pathspec') &&
e.message.includes('did not match any files') &&
logWarning
)
core.warning(
`Add command did not match any file:\n git add ${args}`
)
else throw e
e.message.includes('did not match any files')
) {
if (ignoreErrors == 'pathspec') return

const peh = getInput('pathspec_error_handling'),
err = new Error(
`Add command did not match any file: git add ${args}`
)
if (peh == 'exitImmediately') throw err
if (peh == 'exitAtEnd') exitErrors.push(err)
} else throw e
})
)
}

return res
}

async function remove({
logWarning = true,
ignoreErrors = false
} = {}): Promise<(void | Response<void>)[]> {
async function remove(
ignoreErrors: 'all' | 'pathspec' | 'none' = 'none'
): Promise<(void | Response<void>)[]> {
const input = getInput('remove')
if (!input) return []

Expand All @@ -427,19 +457,26 @@ async function remove({
// If any of them fails, the whole function will return a Promise rejection
await git
.rm(matchGitArgs(args), (e: any, d?: any) =>
log(ignoreErrors ? null : e, d)
log(ignoreErrors == 'all' ? null : e, d)
)
.catch((e: Error) => {
if (ignoreErrors) return
// if I should ignore every error, return
if (ignoreErrors == 'all') return

// if it's a pathspec error...
if (
e.message.includes('fatal: pathspec') &&
e.message.includes('did not match any files')
)
logWarning &&
core.warning(
) {
if (ignoreErrors == 'pathspec') return

const peh = getInput('pathspec_error_handling'),
err = new Error(
`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
})
)
}
Expand Down
80 changes: 49 additions & 31 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,64 @@ import * as core from '@actions/core'
import { Toolkit } from 'actions-toolkit'
import fs from 'fs'

export type Input =
| 'add'
| 'author_name'
| 'author_email'
| 'branch'
| 'committer_name'
| 'committer_email'
| 'cwd'
| 'default_author'
| 'message'
| 'pull_strategy'
| 'push'
| 'remove'
| 'signoff'
| 'tag'
| 'github_token'

export type Output = 'committed' | 'commit_sha' | 'pushed' | 'tagged'
interface InputTypes {
add: string
author_name: string
author_email: string
branch: string
committer_name: string
committer_email: string
cwd: string
default_author: 'github_actor' | 'user_info' | 'github_actions'
message: string
pathspec_error_handling: 'ignore' | 'exitImmediately' | 'exitAtEnd'
pull_strategy: string
push: string
remove: string | undefined
signoff: undefined
tag: string | undefined

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>
export const tools = new Toolkit<RecordOf<Input>, RecordOf<Output>>({
export const tools = new Toolkit<RecordOf<input>, RecordOf<output>>({
secrets: [
'GITHUB_EVENT_PATH',
'GITHUB_EVENT_NAME',
'GITHUB_REF',
'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(name: Input, bool?: false): string
export function getInput(name: Input, bool = false) {
if (bool) return core.getBooleanInput(name)
return tools.inputs[name] || ''
export function getInput<T extends input>(name: T, parseAsBool: true): boolean
export function getInput<T extends input>(
name: T,
parseAsBool?: false
): 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) {
Expand Down Expand Up @@ -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}`)
outputs[name] = value
core.setOutput(name, value)
Expand Down