Skip to content
Open
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
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,14 @@ Add a step like this to your workflow:

# The message for the commit.
# Default: 'Commit from GitHub Actions (name of the workflow)'
# You can also provide multiple messages as a YAML/JSON array to create multiple commits
message: 'Your commit message'

# Template for commit message. Use 'reuse' to reuse the previous commit's message,
# or use '{original}' placeholder in your message to include the previous commit message
# Default: ''
message_template: 'reuse'

# If this input is set, the action will push the commit to a new branch with this name.
# Default: ''
new_branch: custom-new-branch
Expand Down Expand Up @@ -257,10 +263,64 @@ You can also use the `committer_name` and `committer_email` inputs to make it ap
committer_email: 41898282+github-actions[bot]@users.noreply.github.com
```

### Multiple commits and reusing commit messages

You can create multiple commits in a single action run by providing an array of commit messages:

```yaml
- uses: EndBug/add-and-commit@v9
with:
message: |
- First commit message
- Second commit message
- Third commit message
```

#### Reusing previous commit messages

You can reuse the previous commit's message exactly using the `message_template` input:

```yaml
- uses: EndBug/add-and-commit@v9
with:
message_template: 'reuse'
```

#### Templating with previous commit messages

Use the `{original}` placeholder in `message_template` to include the previous commit message with additional text:

**Suffix the previous message** (useful for formatting commits):
```yaml
- uses: EndBug/add-and-commit@v9
with:
message_template: '{original}\n\nFormatted code with prettier'
```

**Prefix the previous message**:
```yaml
- uses: EndBug/add-and-commit@v9
with:
message_template: 'chore: {original}'
```

**Wrap the previous message**:
```yaml
- uses: EndBug/add-and-commit@v9
with:
message_template: 'build: {original}\n\nCo-authored-by: bot@example.com'
```

This is particularly useful for automated workflows that need to preserve the original commit message while adding context like:
- Formatting/linting notifications
- Build system updates
- Automated fixes
- Co-author attributions

### Array inputs

Due to limitations in the GitHub action APIs, all inputs must be either strings or booleans.
The action supports arrays in `add` and `remove`, but they have to be encoded as a string with a YAML flow sequence:
The action supports arrays in `add`, `remove`, and now `message`, but they have to be encoded as a string with a YAML flow sequence:

```yaml
- uses: EndBug/add-and-commit@v9
Expand All @@ -280,6 +340,14 @@ The action supports arrays in `add` and `remove`, but they have to be encoded as

(Note the pipe character making it a multiline string.)

The same applies to the `message` input when creating multiple commits:

```yaml
- uses: EndBug/add-and-commit@v9
with:
message: '["First commit", "Second commit"]'
```

### Automated linting

Do you want to lint your JavaScript files, located in the `src` folder, with ESLint, so that fixable changes are done without your intervention? You can use a workflow like this:
Expand Down
5 changes: 4 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ inputs:
required: false
default: --tags --force
message:
description: The message for the commit
description: The message for the commit (can be a string, array of strings for multiple commits)
required: false
message_template:
description: Template for commit message. Use 'reuse' to reuse the previous commit's message exactly, or use '{original}' as a placeholder to include the previous message (e.g., '{original}\n\nFormatted code' to suffix, or 'chore: {original}' to prefix)
required: false
Comment on lines +37 to 41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix YAML syntax error in message_template description.

The plain-scalar description contains chore: {original} (colon + space), which breaks YAML parsing and invalidates the action metadata. Use a block scalar to keep the text intact.

🛠️ Proposed fix
   message_template:
-    description: Template for commit message. Use 'reuse' to reuse the previous commit's message exactly, or use '{original}' as a placeholder to include the previous message (e.g., '{original}\n\nFormatted code' to suffix, or 'chore: {original}' to prefix)
+    description: >-
+      Template for commit message. Use 'reuse' to reuse the previous commit's
+      message exactly, or use '{original}' as a placeholder to include the
+      previous message (e.g., '{original}\n\nFormatted code' to suffix, or
+      'chore: {original}' to prefix)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: The message for the commit (can be a string, array of strings for multiple commits)
required: false
message_template:
description: Template for commit message. Use 'reuse' to reuse the previous commit's message exactly, or use '{original}' as a placeholder to include the previous message (e.g., '{original}\n\nFormatted code' to suffix, or 'chore: {original}' to prefix)
required: false
description: The message for the commit (can be a string, array of strings for multiple commits)
required: false
message_template:
description: >-
Template for commit message. Use 'reuse' to reuse the previous commit's
message exactly, or use '{original}' as a placeholder to include the
previous message (e.g., '{original}\n\nFormatted code' to suffix, or
'chore: {original}' to prefix)
required: false
🧰 Tools
🪛 YAMLlint (1.38.0)

[error] 40-40: syntax error: mapping values are not allowed here

(syntax)

🤖 Prompt for AI Agents
In `@action.yml` around lines 37 - 41, The YAML for the
message_template.description contains unescaped plain text with "chore:
{original}" which breaks parsing; update the message_template description (the
description value under the message_template mapping) to use a block scalar or a
quoted/multiline string so the colon+space and braces are preserved (e.g.,
replace the plain-scalar description with a block scalar indicator or properly
quoted multiline text) and keep the existing explanatory text and examples
intact.

new_branch:
description: The name of the branch to create.
Expand Down
1 change: 1 addition & 0 deletions src/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface InputTypes {
default_author: 'github_actor' | 'user_info' | 'github_actions';
fetch: string;
message: string;
message_template: string | undefined;
new_branch: string | undefined;
pathspec_error_handling: 'ignore' | 'exitImmediately' | 'exitAtEnd';
pull: string | undefined;
Expand Down
89 changes: 80 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,86 @@ core.info(`Running in ${baseDir}`);
} else core.info('> Not pulling from repo.');

core.info('> Creating commit...');
await git
.commit(getInput('message'), matchGitArgs(getInput('commit') || ''))
.then(async data => {
log(undefined, data);
setOutput('committed', 'true');
setOutput('commit_long_sha', data.commit);
setOutput('commit_sha', data.commit.substring(0, 7));
})
.catch(err => core.setFailed(err));

// Get commit messages - support for arrays and message templates
let messages = parseInputArray(getInput('message'));
const messageTemplate = getInput('message_template');

// Handle message template (reuse previous commit)
if (messageTemplate) {
try {
const previousCommit = await git.log(['-1']);
const previousMessage = previousCommit.latest?.message || '';

if (messageTemplate === 'reuse') {
// Reuse the entire previous commit message
messages = [previousMessage];
core.info(`> Reusing previous commit message: "${previousMessage}"`);
} else if (messageTemplate.includes('{original}')) {
// Replace {original} placeholder with previous message in the template
const templatedMessage = messageTemplate.replace(/\{original\}/g, previousMessage);
messages = [templatedMessage];
Comment on lines +132 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let me check the structure and find src/main.ts
git ls-files | grep -E "src/main\.(ts|js)$"

Repository: EndBug/add-and-commit

Length of output: 76


🏁 Script executed:

# Read the relevant section of src/main.ts around lines 132-135
head -n 150 src/main.ts | tail -n 50 | cat -n

Repository: EndBug/add-and-commit

Length of output: 2395


🏁 Script executed:

# Get more context around the template replacement to understand previousMessage source
sed -n '100,160p' src/main.ts | cat -n

Repository: EndBug/add-and-commit

Length of output: 3093


Escape replacement to avoid $-expansion in templates.

String.replace() with a string replacement interprets $&, $$, and $n sequences specially. If a previous commit message contains these sequences, they will be misinterpreted—for example, $& will be replaced with the matched text {original} instead of preserving the literal message. Use a replacer function to prevent this.

🛠️ Proposed fix
-          const templatedMessage = messageTemplate.replace(/\{original\}/g, previousMessage);
+          const templatedMessage = messageTemplate.replace(
+            /\{original\}/g,
+            () => previousMessage,
+          );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (messageTemplate.includes('{original}')) {
// Replace {original} placeholder with previous message in the template
const templatedMessage = messageTemplate.replace(/\{original\}/g, previousMessage);
messages = [templatedMessage];
} else if (messageTemplate.includes('{original}')) {
// Replace {original} placeholder with previous message in the template
const templatedMessage = messageTemplate.replace(
/\{original\}/g,
() => previousMessage,
);
messages = [templatedMessage];
🤖 Prompt for AI Agents
In `@src/main.ts` around lines 132 - 135, The replacement of the {original}
placeholder uses String.replace with a string replacement which can misinterpret
$ sequences in previousMessage; update the logic around
messageTemplate.replace(...) (the variables messageTemplate and previousMessage
and the messages = [...] assignment) to call String.replace with a replacer
function that returns previousMessage (instead of a replacement string) so that
any $ characters in previousMessage are treated literally and not expanded by
the engine.

core.info(`> Using message template: "${templatedMessage}"`);
}
} catch (err) {
core.warning(`Could not retrieve previous commit message: ${err}`);
}
}

// Create commits (one or multiple)
let commitFailed = false;
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
const isLastCommit = i === messages.length - 1;

if (messages.length > 1) {
core.info(`> Creating commit ${i + 1}/${messages.length}: "${message}"`);
}

// Check for staged changes before attempting commit
const stagedChanges = (await git.diffSummary(['--cached'])).files.length;
const commitArgs = matchGitArgs(getInput('commit') || '');
const allowEmpty = commitArgs.includes('--allow-empty');

if (stagedChanges === 0 && !allowEmpty) {
const errorMsg = `Cannot create commit ${i + 1}/${messages.length}: No staged changes found. Either stage files using add/remove inputs or use '--allow-empty' in the commit input to create empty commits.`;
core.setFailed(errorMsg);
break;
}

core.debug(
`Commit ${i + 1}: ${stagedChanges} staged files, --allow-empty: ${allowEmpty}`,
);

await git
.commit(message, commitArgs)
.then(async data => {
log(undefined, data);

// Only set outputs for the last commit
if (isLastCommit) {
setOutput('committed', 'true');
setOutput('commit_long_sha', data.commit);
setOutput('commit_sha', data.commit.substring(0, 7));
}
})
.catch(err => {
core.setFailed(err);
commitFailed = true;
});

// Stop processing if commit failed
if (commitFailed) {
break;
}

// Re-stage files for subsequent commits if there are more to create
if (!isLastCommit) {
core.info('> Re-staging files for next commit...');
if (getInput('add')) await add(ignoreErrors);
if (getInput('remove')) await remove(ignoreErrors);
}
}

if (getInput('tag')) {
core.info('> Tagging commit...');
Expand Down