diff --git a/README.md b/README.md index 5154fe5f..e21a43bc 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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: diff --git a/action.yml b/action.yml index 2acc1e7d..61a7254e 100644 --- a/action.yml +++ b/action.yml @@ -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 new_branch: description: The name of the branch to create. diff --git a/src/io.ts b/src/io.ts index 26259239..8dccd034 100644 --- a/src/io.ts +++ b/src/io.ts @@ -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; diff --git a/src/main.ts b/src/main.ts index 58d8c790..5cbf6042 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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]; + 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...');