Repository navigation
feat: add create-signed-commit composite action - #320
Conversation
| maxBuffer: 100 * 1024 * 1024, | ||
| }); | ||
|
|
||
| const { data: blob } = await github.rest.git.createBlob({ |
There was a problem hiding this comment.
I've never had to use these parts of the GitHub API. Interesting 🤔
There was a problem hiding this comment.
Me neither. 😅 But it seems to be the easiest way to create signed commits. The other alternative is the GraphQL createCommitOnBranch mutation, but that doesn't support symlinks or executable files from my understanding. Maybe an edge case, but I want to keep support in this action as broad as possible.
|
|
||
| // The author and committer are deliberately omitted. GitHub only | ||
| // signs the commit when it sets them itself. | ||
| const { data: commit } = await github.rest.git.createCommit({ |
There was a problem hiding this comment.
What if this workflow stops midway through? It seems like we would create objects in the repo but they would be orphaned. Is that a concern?
There was a problem hiding this comment.
I don't think it's a concern. They will just be garbage collected by GitHub automatically.
Commit the staged changes through the GitHub REST Git Data API, so that GitHub signs the commit on behalf of the token owner, such as a GitHub App. `git commit -S` can't do this because a token has no private key. The action uses the REST API instead of `createCommitOnBranch` because it preserves file modes and symlinks and fits any number of changes in one commit. The `branch` input is required, so a workflow can't push to the default branch by accident.
Move the final `git fetch` and `git reset` out of the `github-script` step into their own step, and pass the token to the fetch in the URL. The action no longer depends on `actions/checkout` persisting credentials, so callers can use `persist-credentials: false`. The new step is skipped when there is nothing to commit.
The action listed staged paths with `git diff --cached` but read contents, modes, and deletions from the working tree. As a result, it could commit unstaged edits, commit a `git rm --cached` removal as a modification, and ignore index modes and clean-filter or LFS blobs. Now the action reads the mode, blob SHA, and status of each change from `git diff --cached --raw` and uploads the staged blob with `git cat-file`. It parses the output with a named-group regex instead of pairing fields by position.
The sync step passes the token in the fetch URL, but the default `actions/checkout` persists its own `Authorization` header in `.git/config`. If the first request is challenged, git can send both headers, and GitHub rejects the duplicate. This happens after the signed commit has already landed. Pass `-c http.https://github.com/.extraheader=` to the fetch so only the token from the URL is sent, whichever token the checkout used.
03bdbda to
9f7f0a5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9f7f0a5. Configure here.
The sync step ran `git reset` on whatever HEAD pointed at. When the signed commit was published to a different branch, the checked-out local branch ended up ahead of its remote, and a later `git push` could publish the commit there too. The fetch by SHA also left the remote-tracking branch stale. Fetch the published branch into `refs/remotes/origin/<branch>`, and reset only if HEAD is detached or on that branch. In any other case the checkout stays unchanged, with the changes still staged.
| core.info('No changes to commit.'); | ||
| core.setOutput('commit-sha', ''); | ||
| return; |
There was a problem hiding this comment.
Is this preferable to failing or skipping?
There was a problem hiding this comment.
Not sure. git commit does exit with 1 if there's nothing to commit, but I feel like this is easier to handle? Thinking about a changelog update workflow for example, we can just call this action regardless of whether there are changes to the changelog or not.
There was a problem hiding this comment.
We could, it just seems harder to understand in the GitHub UI as it would show up as "completed/passed"
There was a problem hiding this comment.
That seems fine to me? Otherwise we'd need to set continue-on-error or check beforehand if there's anything to commit, which we do right now and exit with 0:

Adds a composite action that commits the staged changes through the GitHub REST Git Data API. GitHub signs commits created this way on behalf of the token owner, so a GitHub App installation token produces verified commits.
git commit -Scan't do this because a token has no private key.The action detects the staged changes with
git diff --cached, uploads each file as a blob (keeping file modes and symlinks), creates a tree and a commit with no author or committer set, and then updates the branch ref without forcing. It creates the branch if it doesn't exist on the remote, and it fails if the remote branch isn't at the checked-out commit. Afterwards it resets the local index onto the signed commit, so later steps don't see the committed changes as pending.Callers must stage the files with
git addfirst. The token is passed to the finalgit fetchdirectly, so the checkout can usepersist-credentials: false.Note
Low Risk
Adds a new reusable CI action only; no runtime app or auth logic changes beyond how workflows commit via the GitHub API.
Overview
Introduces a new composite GitHub Action Create signed commit so workflows can push GitHub-verified commits using a token (e.g. a GitHub App installation token) instead of local
git commit -S.Callers stage changes with
git add, then the action reads the index viagit diff --cached, uploads blobs through the Git Data API, builds a commit without author/committer (so GitHub sets and signs them), and updates or creates the target branch ref without force-push. It errors when the remote branch is ahead of the checkout, skips empty stages, and does not support submodules. A follow-up step fetches the branch and resets the local checkout onto the new SHA when HEAD is detached or on that branch.Outputs
commit-shafor downstream steps.Reviewed by Cursor Bugbot for commit 6b4d97b. Bugbot is set up for automated code reviews on this repo. Configure here.