Skip to content

Commit 05d45f3

Browse files
authored
[BRE-1534] Adding api commit action (#626)
1 parent 7b53633 commit 05d45f3

3 files changed

Lines changed: 497 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
name: Test API Commit Action
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- "api-commit/**"
8+
- ".github/workflows/test-api-commit.yml"
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
test-api:
15+
name: Test API commit (${{ matrix.mode }})
16+
runs-on: ubuntu-24.04
17+
permissions:
18+
contents: write
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
mode:
23+
- explicit-files
24+
- auto-detect-files
25+
- no-changes
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
with:
30+
persist-credentials: false
31+
32+
- name: Create test branch
33+
id: setup
34+
env:
35+
GH_TOKEN: ${{ github.token }}
36+
REPOSITORY: ${{ github.repository }}
37+
run: |
38+
TEST_BRANCH="api-commit-${{ matrix.mode }}-$(date +%s)"
39+
echo "test_branch=$TEST_BRANCH" >> "$GITHUB_OUTPUT"
40+
41+
SHA=$(gh api "repos/$REPOSITORY/git/ref/heads/main" --jq '.object.sha')
42+
gh api "repos/$REPOSITORY/git/refs" \
43+
--method POST \
44+
-f "ref=refs/heads/$TEST_BRANCH" \
45+
-f "sha=$SHA"
46+
47+
- name: Create test files (explicit files)
48+
if: matrix.mode == 'explicit-files'
49+
run: |
50+
echo "content $(date +%s)" > api-commit-test-file-1.txt
51+
echo "content $(date +%s)" > api-commit-test-file-2.txt
52+
53+
- name: Run API Commit action (explicit files)
54+
id: api-commit-explicit
55+
if: matrix.mode == 'explicit-files'
56+
uses: ./api-commit
57+
with:
58+
files: |
59+
api-commit-test-file-1.txt
60+
api-commit-test-file-2.txt
61+
message: "chore: test API commit"
62+
branch: ${{ steps.setup.outputs.test_branch }}
63+
token: ${{ github.token }}
64+
65+
- name: Create and stage test file (auto-detect files)
66+
if: matrix.mode == 'auto-detect-files'
67+
run: |
68+
echo "content $(date +%s)" > api-commit-auto-test-file.txt
69+
git add api-commit-auto-test-file.txt
70+
71+
- name: Run API Commit action (auto-detect files)
72+
id: api-commit-auto
73+
if: matrix.mode == 'auto-detect-files'
74+
uses: ./api-commit
75+
with:
76+
message: "chore: test API commit auto-detect"
77+
branch: ${{ steps.setup.outputs.test_branch }}
78+
token: ${{ github.token }}
79+
80+
- name: Run API Commit action (no changes)
81+
id: api-commit-no-changes
82+
if: matrix.mode == 'no-changes'
83+
uses: ./api-commit
84+
with:
85+
message: "chore: test API commit no changes"
86+
branch: ${{ steps.setup.outputs.test_branch }}
87+
token: ${{ github.token }}
88+
89+
- name: Get commit SHA
90+
id: commit
91+
env:
92+
EXPLICIT_FILES_SHA: ${{ steps.api-commit-explicit.outputs.commit_sha }}
93+
EXPECTED_EXPLICIT_FILES: "api-commit-test-file-1.txt,api-commit-test-file-2.txt"
94+
AUTO_DETECT_FILES_SHA: ${{ steps.api-commit-auto.outputs.commit_sha }}
95+
EXPECTED_AUTO_DETECT_FILES: "api-commit-auto-test-file.txt"
96+
NO_CHANGES_OUTCOME: ${{ steps.api-commit-no-changes.outcome }}
97+
run: |
98+
if [[ -n "$EXPLICIT_FILES_SHA" ]]; then
99+
echo "sha=$EXPLICIT_FILES_SHA" >> "$GITHUB_OUTPUT"
100+
echo "expected_files=$EXPECTED_EXPLICIT_FILES" >> "$GITHUB_OUTPUT"
101+
elif [[ -n "$AUTO_DETECT_FILES_SHA" ]]; then
102+
echo "sha=$AUTO_DETECT_FILES_SHA" >> "$GITHUB_OUTPUT"
103+
echo "expected_files=$EXPECTED_AUTO_DETECT_FILES" >> "$GITHUB_OUTPUT"
104+
elif [[ "$NO_CHANGES_OUTCOME" == "success" ]]; then
105+
echo "sha=" >> "$GITHUB_OUTPUT"
106+
echo "expected_files=" >> "$GITHUB_OUTPUT"
107+
else
108+
echo "::error::No commit SHA found"
109+
exit 1
110+
fi
111+
112+
- name: Validate commit was created
113+
env:
114+
COMMIT_SHA: ${{ steps.commit.outputs.sha }}
115+
EXPECTED_FILES: ${{ steps.commit.outputs.expected_files }}
116+
BRANCH: ${{ steps.setup.outputs.test_branch }}
117+
GH_TOKEN: ${{ github.token }}
118+
REPOSITORY: ${{ github.repository }}
119+
run: |
120+
if [[ -z "$EXPECTED_FILES" ]]; then
121+
if [[ -n "$COMMIT_SHA" ]]; then
122+
echo "::error::Expected no commit to be made, but got commit_sha=$COMMIT_SHA"
123+
exit 1
124+
fi
125+
echo "Correctly produced no commit"
126+
exit 0
127+
fi
128+
129+
echo "Commit SHA from action: $COMMIT_SHA"
130+
131+
if [[ -z "$COMMIT_SHA" ]]; then
132+
echo "::error::commit_sha output was empty"
133+
exit 1
134+
fi
135+
136+
REMOTE_SHA=$(git ls-remote origin "refs/heads/$BRANCH" | cut -f1)
137+
echo "Remote branch HEAD SHA: $REMOTE_SHA"
138+
139+
if [[ "$REMOTE_SHA" != "$COMMIT_SHA" ]]; then
140+
echo "::error::SHA mismatch: action returned $COMMIT_SHA but branch HEAD is $REMOTE_SHA"
141+
exit 1
142+
fi
143+
144+
CHANGED_FILES=$(gh api "repos/$REPOSITORY/commits/$COMMIT_SHA" --jq '[.files[].filename] | sort | join(",")')
145+
echo "Files changed in commit: $CHANGED_FILES"
146+
147+
if [[ "$CHANGED_FILES" != "$EXPECTED_FILES" ]]; then
148+
echo "::error::Expected $EXPECTED_FILES in commit, got: $CHANGED_FILES"
149+
exit 1
150+
fi
151+
152+
echo "Commit verified successfully"
153+
154+
- name: Check commit verification status
155+
if: steps.commit.outputs.sha != ''
156+
env:
157+
GH_TOKEN: ${{ github.token }}
158+
COMMIT_SHA: ${{ steps.commit.outputs.sha }}
159+
REPOSITORY: ${{ github.repository }}
160+
run: |
161+
# GitHub automatically signs commits created via the Git Data API,
162+
# so verified=true is expected regardless of which token is used.
163+
VERIFIED=$(gh api "repos/$REPOSITORY/commits/$COMMIT_SHA" --jq '.commit.verification.verified')
164+
REASON=$(gh api "repos/$REPOSITORY/commits/$COMMIT_SHA" --jq '.commit.verification.reason')
165+
echo "Verified: $VERIFIED (reason: $REASON)"
166+
167+
if [[ "$VERIFIED" != "true" ]]; then
168+
echo "::error::Expected commit to be verified, got verified=$VERIFIED reason=$REASON"
169+
exit 1
170+
fi
171+
172+
- name: Cleanup test branch
173+
if: always()
174+
env:
175+
GH_TOKEN: ${{ github.token }}
176+
BRANCH: ${{ steps.setup.outputs.test_branch }}
177+
REPOSITORY: ${{ github.repository }}
178+
run: |
179+
if [[ -n "$BRANCH" ]]; then
180+
gh api "repos/$REPOSITORY/git/refs/heads/$BRANCH" --method DELETE
181+
fi

api-commit/README.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# API Commit Action
2+
3+
Create a verified commit via the GitHub API without requiring `git` credentials on the runner.
4+
5+
## Features
6+
7+
- Creates commits via the GitHub REST API (Git Data API)
8+
- Supports GitHub App tokens for commits verified as a GitHub App identity
9+
- Commits multiple files atomically in a single commit
10+
- Exposes the resulting commit SHA as an output
11+
12+
## Inputs
13+
14+
| Input | Description | Required | Default |
15+
| --------- | ------------------------------------------------------------------------------ | -------- | ------------------------ |
16+
| `files` | Newline-delimited list of files to commit. If omitted, all files modified relative to HEAD are committed. | No | - |
17+
| `message` | Commit message | Yes | - |
18+
| `branch` | Branch to commit to | No | `${{ github.ref }}` |
19+
| `token` | GitHub token for API access. Use a GitHub App token for verified commits. | Yes | - |
20+
21+
## Outputs
22+
23+
| Output | Description |
24+
| ------------ | ----------------------------- |
25+
| `commit_sha` | SHA of the created commit, or empty if no changed files were detected |
26+
27+
## Usage
28+
29+
### Auto-detect Changed Files
30+
31+
Omit `files` to automatically commit all files modified relative to HEAD:
32+
33+
```yaml
34+
- name: Commit all changed files
35+
uses: bitwarden/gh-actions/api-commit@main
36+
with:
37+
message: "chore: update generated files"
38+
token: ${{ secrets.GITHUB_TOKEN }}
39+
```
40+
41+
### Explicit File List
42+
43+
```yaml
44+
- name: Commit changed files
45+
uses: bitwarden/gh-actions/api-commit@main
46+
with:
47+
files: |
48+
package.json
49+
package-lock.json
50+
message: "chore: bump version to ${{ inputs.version }}"
51+
token: ${{ secrets.GITHUB_TOKEN }}
52+
```
53+
54+
### With a GitHub App Token
55+
56+
Pass a GitHub App token to create commits verified as a GitHub App identity:
57+
58+
```yaml
59+
- name: Generate GitHub App token
60+
id: app-token
61+
uses: actions/create-github-app-token@v1
62+
with:
63+
app-id: ${{ secrets.APP_ID }}
64+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
65+
66+
- name: Commit changed files
67+
uses: bitwarden/gh-actions/api-commit@main
68+
with:
69+
files: version.txt
70+
message: "chore: update version"
71+
token: ${{ steps.app-token.outputs.token }}
72+
```
73+
74+
### Specifying a Branch
75+
76+
```yaml
77+
- name: Commit to a specific branch
78+
uses: bitwarden/gh-actions/api-commit@main
79+
with:
80+
files: CHANGELOG.md
81+
message: "docs: update changelog"
82+
branch: release/2024.1
83+
token: ${{ secrets.GITHUB_TOKEN }}
84+
```
85+
86+
### Using the Commit SHA Output
87+
88+
```yaml
89+
- name: Commit changed files
90+
id: api-commit
91+
uses: bitwarden/gh-actions/api-commit@main
92+
with:
93+
files: version.txt
94+
message: "chore: bump version"
95+
token: ${{ secrets.GITHUB_TOKEN }}
96+
97+
- name: Print commit SHA
98+
run: echo "Created commit ${{ steps.api-commit.outputs.commit_sha }}"
99+
```
100+
101+
## Requirements
102+
103+
- The target `branch` must already exist. The action looks up the branch HEAD via the API as its first step and will fail with a 404 if the branch does not exist.
104+
- File deletions are not supported. The Git Data API requires a separate approach to remove files from a tree. When using explicit `files`, listing a deleted file will fail with "File not found". When using auto-detect, deleted files are silently excluded from the commit.
105+
- Auto-detect mode requires a local git repository with a `HEAD` commit (i.e., `actions/checkout` must have run). Explicit `files` mode has no git dependency — files are read directly from disk regardless of git state.
106+
107+
## Permissions
108+
109+
Requires `contents: write` permission on the calling job:
110+
111+
```yaml
112+
permissions:
113+
contents: write
114+
```

0 commit comments

Comments
 (0)