Skip to content

Commit 4d9c6e9

Browse files
committed
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
1 parent 68050c9 commit 4d9c6e9

File tree

5 files changed

+150
-84
lines changed

5 files changed

+150
-84
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ Add a step like this to your workflow:
5454
# Default: 'Commit from GitHub Actions (name of the workflow)'
5555
message: 'Your commit message'
5656

57+
# The way the action should handle pathspec errors from the add and remove commands. Three options are available:
58+
# - ignore -> errors will be logged but the step won't fail
59+
# - exitImmediately -> the action will stop right away, and the step will fail
60+
# - exitAtEnd -> the action will go on, every pathspec error will be logged at the end, the step will fail.
61+
# Default: ignore
62+
pathspec_error_handling: ignore
63+
5764
# The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all.
5865
# Default: '--no-rebase'
5966
pull_strategy: 'NO-PULL or --no-rebase or --no-ff or --rebase'

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ inputs:
3232
message:
3333
description: The message for the commit
3434
required: false
35+
pathspec_error_handling:
36+
description: The way the action should handle pathspec errors from the add and remove commands.
37+
required: false
38+
default: ignore
3539
pull_strategy:
3640
description: The flag used on the pull strategy. Use NO-PULL to avoid the action pulling at all.
3741
required: false

lib/index.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 87 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import YAML from 'js-yaml'
55
import {
66
getInput,
77
getUserInfo,
8-
Input,
8+
input,
99
log,
1010
matchGitArgs,
1111
outputs,
@@ -15,21 +15,25 @@ import {
1515

1616
const baseDir = path.join(process.cwd(), getInput('cwd') || '')
1717
const git = simpleGit({ baseDir })
18+
19+
const exitErrors: Error[] = []
20+
1821
core.info(`Running in ${baseDir}`)
1922
;(async () => {
2023
await checkInputs().catch(core.setFailed)
2124

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

28+
const peh = getInput('pathspec_error_handling')
2529
if (getInput('add')) {
2630
core.info('> Adding files...')
27-
await add()
31+
await add(peh == 'ignore' ? 'pathspec' : 'none')
2832
} else core.info('> No files to add.')
2933

3034
if (getInput('remove')) {
3135
core.info('> Removing files...')
32-
await remove()
36+
await remove(peh == 'ignore' ? 'pathspec' : 'none')
3337
} else core.info('> No files to remove.')
3438

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

7377
core.info('> Re-staging files...')
74-
if (getInput('add')) await add({ ignoreErrors: true })
75-
if (getInput('remove')) await remove({ ignoreErrors: true })
78+
if (getInput('add')) await add('all')
79+
if (getInput('remove')) await remove('all')
7680

7781
core.info('> Creating commit...')
7882
await git.commit(
@@ -97,7 +101,7 @@ core.info(`Running in ${baseDir}`)
97101
if (getInput('tag')) {
98102
core.info('> Tagging commit...')
99103
await git
100-
.tag(matchGitArgs(getInput('tag')), (err, data?) => {
104+
.tag(matchGitArgs(getInput('tag') || ''), (err, data?) => {
101105
if (data) setOutput('tagged', 'true')
102106
return log(err, data)
103107
})
@@ -159,7 +163,7 @@ core.info(`Running in ${baseDir}`)
159163
{
160164
'--delete': null,
161165
origin: null,
162-
[matchGitArgs(getInput('tag')).filter(
166+
[matchGitArgs(getInput('tag') || '').filter(
163167
(w) => !w.startsWith('-')
164168
)[0]]: null
165169
},
@@ -177,6 +181,14 @@ core.info(`Running in ${baseDir}`)
177181
core.info('> Working tree clean. Nothing to commit.')
178182
}
179183
})()
184+
.then(() => {
185+
// Check for exit errors
186+
if (exitErrors.length == 1) throw exitErrors[0]
187+
else if (exitErrors.length > 1) {
188+
exitErrors.forEach((e) => core.error(e))
189+
throw 'There have been multiple runtime errors.'
190+
}
191+
})
180192
.then(logOutputs)
181193
.catch((e) => {
182194
core.endGroup()
@@ -185,11 +197,11 @@ core.info(`Running in ${baseDir}`)
185197
})
186198

187199
async function checkInputs() {
188-
function setInput(input: Input, value: string | undefined) {
200+
function setInput(input: input, value: string | undefined) {
189201
if (value) return (process.env[`INPUT_${input.toUpperCase()}`] = value)
190202
else return delete process.env[`INPUT_${input.toUpperCase()}`]
191203
}
192-
function setDefault(input: Input, value: string) {
204+
function setDefault(input: input, value: string) {
193205
if (!getInput(input)) setInput(input, value)
194206
return getInput(input)
195207
}
@@ -219,7 +231,7 @@ async function checkInputs() {
219231
else core.setFailed('Add input: array length < 1')
220232
}
221233
if (getInput('remove')) {
222-
const parsed = parseInputArray(getInput('remove'))
234+
const parsed = parseInputArray(getInput('remove') || '')
223235
if (parsed.length == 1)
224236
core.info(
225237
'Remove input parsed as single string, running 1 git rm command.'
@@ -327,25 +339,16 @@ async function checkInputs() {
327339
core.info(`> Running for a PR, the action will use '${branch}' as ref.`)
328340
// #endregion
329341

330-
// #region signoff
331-
if (getInput('signoff')) {
332-
const parsed = getInput('signoff', true)
333-
334-
if (parsed === undefined)
335-
throw new Error(
336-
`"${getInput(
337-
'signoff'
338-
)}" is not a valid value for the 'signoff' input: only "true" and "false" are allowed.`
339-
)
340-
341-
if (!parsed) setInput('signoff', undefined)
342-
343-
core.debug(
344-
`Current signoff option: ${getInput('signoff')} (${typeof getInput(
345-
'signoff'
346-
)})`
342+
// #region pathspec_error_handling
343+
const peh_valid = ['ignore', 'exitImmediately', 'exitAtEnd']
344+
if (!peh_valid.includes(getInput('pathspec_error_handling')))
345+
throw new Error(
346+
`"${getInput(
347+
'pathspec_error_handling'
348+
)}" is not a valid value for the 'pathspec_error_handling' input. Valid values are: ${peh_valid.join(
349+
', '
350+
)}`
347351
)
348-
}
349352
// #endregion
350353

351354
// #region pull_strategy
@@ -368,6 +371,27 @@ async function checkInputs() {
368371
}
369372
// #endregion
370373

374+
// #region signoff
375+
if (getInput('signoff')) {
376+
const parsed = getInput('signoff', true)
377+
378+
if (parsed === undefined)
379+
throw new Error(
380+
`"${getInput(
381+
'signoff'
382+
)}" is not a valid value for the 'signoff' input: only "true" and "false" are allowed.`
383+
)
384+
385+
if (!parsed) setInput('signoff', undefined)
386+
387+
core.debug(
388+
`Current signoff option: ${getInput('signoff')} (${typeof getInput(
389+
'signoff'
390+
)})`
391+
)
392+
}
393+
// #endregion
394+
371395
// #region github_token
372396
if (!getInput('github_token'))
373397
core.warning(
@@ -376,9 +400,9 @@ async function checkInputs() {
376400
// #endregion
377401
}
378402

379-
async function add({ logWarning = true, ignoreErrors = false } = {}): Promise<
380-
(void | Response<void>)[]
381-
> {
403+
async function add(
404+
ignoreErrors: 'all' | 'pathspec' | 'none' = 'none'
405+
): Promise<(void | Response<void>)[]> {
382406
const input = getInput('add')
383407
if (!input) return []
384408

@@ -391,30 +415,36 @@ async function add({ logWarning = true, ignoreErrors = false } = {}): Promise<
391415
// If any of them fails, the whole function will return a Promise rejection
392416
await git
393417
.add(matchGitArgs(args), (err: any, data?: any) =>
394-
log(ignoreErrors ? null : err, data)
418+
log(ignoreErrors == 'all' ? null : err, data)
395419
)
396420
.catch((e: Error) => {
397-
if (ignoreErrors) return
421+
// if I should ignore every error, return
422+
if (ignoreErrors == 'all') return
423+
424+
// if it's a pathspec error...
398425
if (
399426
e.message.includes('fatal: pathspec') &&
400-
e.message.includes('did not match any files') &&
401-
logWarning
402-
)
403-
core.warning(
404-
`Add command did not match any file:\n git add ${args}`
405-
)
406-
else throw e
427+
e.message.includes('did not match any files')
428+
) {
429+
if (ignoreErrors == 'pathspec') return
430+
431+
const peh = getInput('pathspec_error_handling'),
432+
err = new Error(
433+
`Add command did not match any file: git add ${args}`
434+
)
435+
if (peh == 'exitImmediately') throw err
436+
if (peh == 'exitAtEnd') exitErrors.push(err)
437+
} else throw e
407438
})
408439
)
409440
}
410441

411442
return res
412443
}
413444

414-
async function remove({
415-
logWarning = true,
416-
ignoreErrors = false
417-
} = {}): Promise<(void | Response<void>)[]> {
445+
async function remove(
446+
ignoreErrors: 'all' | 'pathspec' | 'none' = 'none'
447+
): Promise<(void | Response<void>)[]> {
418448
const input = getInput('remove')
419449
if (!input) return []
420450

@@ -427,19 +457,26 @@ async function remove({
427457
// If any of them fails, the whole function will return a Promise rejection
428458
await git
429459
.rm(matchGitArgs(args), (e: any, d?: any) =>
430-
log(ignoreErrors ? null : e, d)
460+
log(ignoreErrors == 'all' ? null : e, d)
431461
)
432462
.catch((e: Error) => {
433-
if (ignoreErrors) return
463+
// if I should ignore every error, return
464+
if (ignoreErrors == 'all') return
465+
466+
// if it's a pathspec error...
434467
if (
435468
e.message.includes('fatal: pathspec') &&
436469
e.message.includes('did not match any files')
437-
)
438-
logWarning &&
439-
core.warning(
470+
) {
471+
if (ignoreErrors == 'pathspec') return
472+
473+
const peh = getInput('pathspec_error_handling'),
474+
err = new Error(
440475
`Remove command did not match any file:\n git rm ${args}`
441476
)
442-
else throw e
477+
if (peh == 'exitImmediately') throw err
478+
if (peh == 'exitAtEnd') exitErrors.push(err)
479+
} else throw e
443480
})
444481
)
445482
}

src/util.ts

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,64 @@ import * as core from '@actions/core'
33
import { Toolkit } from 'actions-toolkit'
44
import fs from 'fs'
55

6-
export type Input =
7-
| 'add'
8-
| 'author_name'
9-
| 'author_email'
10-
| 'branch'
11-
| 'committer_name'
12-
| 'committer_email'
13-
| 'cwd'
14-
| 'default_author'
15-
| 'message'
16-
| 'pull_strategy'
17-
| 'push'
18-
| 'remove'
19-
| 'signoff'
20-
| 'tag'
21-
| 'github_token'
22-
23-
export type Output = 'committed' | 'commit_sha' | 'pushed' | 'tagged'
6+
interface InputTypes {
7+
add: string
8+
author_name: string
9+
author_email: string
10+
branch: string
11+
committer_name: string
12+
committer_email: string
13+
cwd: string
14+
default_author: 'github_actor' | 'user_info' | 'github_actions'
15+
message: string
16+
pathspec_error_handling: 'ignore' | 'exitImmediately' | 'exitAtEnd'
17+
pull_strategy: string
18+
push: string
19+
remove: string | undefined
20+
signoff: undefined
21+
tag: string | undefined
22+
23+
github_token: string | undefined
24+
}
25+
export type input = keyof InputTypes
26+
27+
interface OutputTypes {
28+
committed: 'true' | 'false'
29+
commit_sha: string | undefined
30+
pushed: 'true' | 'false'
31+
tagged: 'true' | 'false'
32+
}
33+
export type output = keyof OutputTypes
34+
35+
export const outputs: OutputTypes = {
36+
committed: 'false',
37+
commit_sha: undefined,
38+
pushed: 'false',
39+
tagged: 'false'
40+
}
2441

2542
type RecordOf<T extends string> = Record<T, string | undefined>
26-
export const tools = new Toolkit<RecordOf<Input>, RecordOf<Output>>({
43+
export const tools = new Toolkit<RecordOf<input>, RecordOf<output>>({
2744
secrets: [
2845
'GITHUB_EVENT_PATH',
2946
'GITHUB_EVENT_NAME',
3047
'GITHUB_REF',
3148
'GITHUB_ACTOR'
3249
]
3350
})
34-
export const outputs: Record<Output, any> = {
35-
committed: 'false',
36-
commit_sha: undefined,
37-
pushed: 'false',
38-
tagged: 'false'
39-
}
4051

41-
export function getInput(name: Input, bool: true): boolean
42-
export function getInput(name: Input, bool?: false): string
43-
export function getInput(name: Input, bool = false) {
44-
if (bool) return core.getBooleanInput(name)
45-
return tools.inputs[name] || ''
52+
export function getInput<T extends input>(name: T, parseAsBool: true): boolean
53+
export function getInput<T extends input>(
54+
name: T,
55+
parseAsBool?: false
56+
): InputTypes[T]
57+
export function getInput<T extends input>(
58+
name: T,
59+
parseAsBool = false
60+
): InputTypes[T] | boolean {
61+
if (parseAsBool) return core.getBooleanInput(name)
62+
// @ts-expect-error
63+
return core.getInput(name)
4664
}
4765

4866
export async function getUserInfo(username?: string) {
@@ -113,7 +131,7 @@ export function readJSON(filePath: string) {
113131
}
114132
}
115133

116-
export function setOutput<T extends Output>(name: T, value: typeof outputs[T]) {
134+
export function setOutput<T extends output>(name: T, value: OutputTypes[T]) {
117135
core.debug(`Setting output: ${name}=${value}`)
118136
outputs[name] = value
119137
core.setOutput(name, value)

0 commit comments

Comments
 (0)