Skip to content

Commit 8c49058

Browse files
authored
[BRE-1534] Adding ability to tag w/ commit and override owner/repo (#628)
1 parent 05d45f3 commit 8c49058

File tree

3 files changed

+127
-18
lines changed

3 files changed

+127
-18
lines changed

.github/workflows/test-api-commit.yml

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
- explicit-files
2424
- auto-detect-files
2525
- no-changes
26+
- tag
2627
steps:
2728
- name: Checkout
2829
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -35,8 +36,10 @@ jobs:
3536
GH_TOKEN: ${{ github.token }}
3637
REPOSITORY: ${{ github.repository }}
3738
run: |
38-
TEST_BRANCH="api-commit-${{ matrix.mode }}-$(date +%s)"
39+
TIMESTAMP=$(date +%s)
40+
TEST_BRANCH="api-commit-${{ matrix.mode }}-$TIMESTAMP"
3941
echo "test_branch=$TEST_BRANCH" >> "$GITHUB_OUTPUT"
42+
echo "test_tag=api-commit-tag-test-$TIMESTAMP" >> "$GITHUB_OUTPUT"
4043
4144
SHA=$(gh api "repos/$REPOSITORY/git/ref/heads/main" --jq '.object.sha')
4245
gh api "repos/$REPOSITORY/git/refs" \
@@ -86,6 +89,22 @@ jobs:
8689
branch: ${{ steps.setup.outputs.test_branch }}
8790
token: ${{ github.token }}
8891

92+
- name: Create test file (tag)
93+
if: matrix.mode == 'tag'
94+
run: |
95+
echo "content $(date +%s)" > api-commit-tag-test-file.txt
96+
97+
- name: Run API Commit action (tag)
98+
id: api-commit-tag
99+
if: matrix.mode == 'tag'
100+
uses: ./api-commit
101+
with:
102+
files: api-commit-tag-test-file.txt
103+
message: "chore: test API commit with tag"
104+
branch: ${{ steps.setup.outputs.test_branch }}
105+
tag_name: ${{ steps.setup.outputs.test_tag }}
106+
token: ${{ github.token }}
107+
89108
- name: Get commit SHA
90109
id: commit
91110
env:
@@ -94,16 +113,26 @@ jobs:
94113
AUTO_DETECT_FILES_SHA: ${{ steps.api-commit-auto.outputs.commit_sha }}
95114
EXPECTED_AUTO_DETECT_FILES: "api-commit-auto-test-file.txt"
96115
NO_CHANGES_OUTCOME: ${{ steps.api-commit-no-changes.outcome }}
116+
TAG_SHA: ${{ steps.api-commit-tag.outputs.commit_sha }}
117+
EXPECTED_TAG_FILES: "api-commit-tag-test-file.txt"
118+
TEST_TAG: ${{ steps.setup.outputs.test_tag }}
97119
run: |
98120
if [[ -n "$EXPLICIT_FILES_SHA" ]]; then
99121
echo "sha=$EXPLICIT_FILES_SHA" >> "$GITHUB_OUTPUT"
100122
echo "expected_files=$EXPECTED_EXPLICIT_FILES" >> "$GITHUB_OUTPUT"
123+
echo "tag_name=" >> "$GITHUB_OUTPUT"
101124
elif [[ -n "$AUTO_DETECT_FILES_SHA" ]]; then
102125
echo "sha=$AUTO_DETECT_FILES_SHA" >> "$GITHUB_OUTPUT"
103126
echo "expected_files=$EXPECTED_AUTO_DETECT_FILES" >> "$GITHUB_OUTPUT"
127+
echo "tag_name=" >> "$GITHUB_OUTPUT"
104128
elif [[ "$NO_CHANGES_OUTCOME" == "success" ]]; then
105129
echo "sha=" >> "$GITHUB_OUTPUT"
106130
echo "expected_files=" >> "$GITHUB_OUTPUT"
131+
echo "tag_name=" >> "$GITHUB_OUTPUT"
132+
elif [[ -n "$TAG_SHA" ]]; then
133+
echo "sha=$TAG_SHA" >> "$GITHUB_OUTPUT"
134+
echo "expected_files=$EXPECTED_TAG_FILES" >> "$GITHUB_OUTPUT"
135+
echo "tag_name=$TEST_TAG" >> "$GITHUB_OUTPUT"
107136
else
108137
echo "::error::No commit SHA found"
109138
exit 1
@@ -169,13 +198,35 @@ jobs:
169198
exit 1
170199
fi
171200
172-
- name: Cleanup test branch
201+
- name: Validate tag was created
202+
if: steps.commit.outputs.tag_name != ''
203+
env:
204+
GH_TOKEN: ${{ github.token }}
205+
COMMIT_SHA: ${{ steps.commit.outputs.sha }}
206+
TAG_NAME: ${{ steps.commit.outputs.tag_name }}
207+
REPOSITORY: ${{ github.repository }}
208+
run: |
209+
TAG_SHA=$(gh api "repos/$REPOSITORY/git/ref/tags/$TAG_NAME" --jq '.object.sha')
210+
echo "Tag $TAG_NAME points to: $TAG_SHA"
211+
212+
if [[ "$TAG_SHA" != "$COMMIT_SHA" ]]; then
213+
echo "::error::Tag SHA mismatch: expected $COMMIT_SHA but tag points to $TAG_SHA"
214+
exit 1
215+
fi
216+
217+
echo "Tag verified successfully"
218+
219+
- name: Cleanup test branch and tag
173220
if: always()
174221
env:
175222
GH_TOKEN: ${{ github.token }}
176223
BRANCH: ${{ steps.setup.outputs.test_branch }}
224+
TAG_NAME: ${{ steps.commit.outputs.tag_name }}
177225
REPOSITORY: ${{ github.repository }}
178226
run: |
179227
if [[ -n "$BRANCH" ]]; then
180228
gh api "repos/$REPOSITORY/git/refs/heads/$BRANCH" --method DELETE
181229
fi
230+
if [[ -n "$TAG_NAME" ]]; then
231+
gh api "repos/$REPOSITORY/git/refs/tags/$TAG_NAME" --method DELETE
232+
fi

api-commit/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ Create a verified commit via the GitHub API without requiring `git` credentials
1313

1414
| Input | Description | Required | Default |
1515
| --------- | ------------------------------------------------------------------------------ | -------- | ------------------------ |
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 | - |
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+
| `tag_name` | Tag to create pointing at the commit. If omitted, no tag is created. If no commit is made, the tag is skipped. | No | - |
21+
| `owner` | Repository owner (org or user). Defaults to the current workflow repository owner. | No | `${{ github.repository_owner }}` |
22+
| `repo` | Repository name. Defaults to the current workflow repository. | No | `${{ github.event.repository.name }}` |
2023

2124
## Outputs
2225

api-commit/action.yml

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ inputs:
2020
token:
2121
description: 'GitHub token for API access. Use a GitHub App token for verified commits.'
2222
required: true
23+
tag_name:
24+
description: 'Tag to create pointing at the commit. If omitted, no tag is created. If no commit is made, the tag is skipped.'
25+
required: false
26+
default: ''
27+
owner:
28+
description: 'Repository owner (org or user). Defaults to the current workflow repository owner.'
29+
required: false
30+
default: ''
31+
repo:
32+
description: 'Repository name. Defaults to the current workflow repository.'
33+
required: false
34+
default: ''
2335

2436
outputs:
2537
commit_sha:
@@ -36,11 +48,16 @@ runs:
3648
INPUT_FILES: ${{ inputs.files }}
3749
INPUT_MESSAGE: ${{ inputs.message }}
3850
INPUT_BRANCH: ${{ inputs.branch }}
51+
INPUT_TAG_NAME: ${{ inputs.tag_name }}
52+
INPUT_OWNER: ${{ inputs.owner }}
53+
INPUT_REPO: ${{ inputs.repo }}
3954
with:
4055
github-token: ${{ inputs.token }}
4156
script: |
4257
const fs = require('fs');
4358
const path = require('path');
59+
const owner = process.env.INPUT_OWNER.trim() || context.repo.owner;
60+
const repo = process.env.INPUT_REPO.trim() || context.repo.repo;
4461
const message = process.env.INPUT_MESSAGE;
4562
4663
// Validate message
@@ -77,6 +94,33 @@ runs:
7794
.filter(f => f.length > 0)
7895
.map(f => path.normalize(f).split(path.sep).join('/'));
7996
} else {
97+
// Verify the local git remote matches the target repository to ensure
98+
// auto-detected files correspond to the correct repository.
99+
const remoteUrl = (await exec.getExecOutput('git', ['remote', 'get-url', 'origin'])).stdout.trim();
100+
let remoteOwner, remoteRepo;
101+
try {
102+
// HTTPS: https://github.com/owner/repo.git
103+
const url = new URL(remoteUrl);
104+
const parts = url.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
105+
remoteOwner = parts[0];
106+
remoteRepo = parts[1];
107+
} catch {
108+
// SSH: git@github.com:owner/repo.git
109+
const match = remoteUrl.match(/[^:]+:([^/]+)\/(.+?)(?:\.git)?$/);
110+
if (match) {
111+
remoteOwner = match[1];
112+
remoteRepo = match[2];
113+
}
114+
}
115+
if (!remoteOwner || !remoteRepo) {
116+
core.setFailed(`Could not parse git remote URL to validate repository: ${remoteUrl}`);
117+
return;
118+
}
119+
if (remoteOwner.toLowerCase() !== owner.toLowerCase() || remoteRepo.toLowerCase() !== repo.toLowerCase()) {
120+
core.setFailed(`Auto-detect requires the local git remote to match the target repository. Remote is ${remoteOwner}/${remoteRepo} but targeting ${owner}/${repo}. Use explicit \`files\` input when targeting a different repository.`);
121+
return;
122+
}
123+
80124
// Combine working tree and staged changes so that staged-only changes are included
81125
const [workingTree, staged] = await Promise.all([
82126
exec.getExecOutput('git', ['diff', '--diff-filter=d', '--name-only', 'HEAD']),
@@ -140,8 +184,8 @@ runs:
140184
mode = (stat.mode & 0o111) ? '100755' : '100644';
141185
}
142186
const { data: blob } = await github.rest.git.createBlob({
143-
owner: context.repo.owner,
144-
repo: context.repo.repo,
187+
owner,
188+
repo,
145189
content: content.toString('base64'),
146190
encoding: 'base64'
147191
});
@@ -150,21 +194,21 @@ runs:
150194
151195
// Get current branch HEAD as late as possible to reduce the race window
152196
const { data: ref } = await github.rest.git.getRef({
153-
owner: context.repo.owner,
154-
repo: context.repo.repo,
197+
owner,
198+
repo,
155199
ref: `heads/${branch}`
156200
});
157201
158202
const { data: commit } = await github.rest.git.getCommit({
159-
owner: context.repo.owner,
160-
repo: context.repo.repo,
203+
owner,
204+
repo,
161205
commit_sha: ref.object.sha
162206
});
163207
164208
// Create tree, commit, and update the branch ref
165209
const { data: newTree } = await github.rest.git.createTree({
166-
owner: context.repo.owner,
167-
repo: context.repo.repo,
210+
owner,
211+
repo,
168212
base_tree: commit.tree.sha,
169213
tree
170214
});
@@ -177,17 +221,17 @@ runs:
177221
}
178222
179223
const { data: newCommit } = await github.rest.git.createCommit({
180-
owner: context.repo.owner,
181-
repo: context.repo.repo,
224+
owner,
225+
repo,
182226
message,
183227
tree: newTree.sha,
184228
parents: [ref.object.sha]
185229
});
186230
187231
try {
188232
await github.rest.git.updateRef({
189-
owner: context.repo.owner,
190-
repo: context.repo.repo,
233+
owner,
234+
repo,
191235
ref: `heads/${branch}`,
192236
sha: newCommit.sha
193237
});
@@ -199,4 +243,15 @@ runs:
199243
throw err;
200244
}
201245
246+
const tagName = process.env.INPUT_TAG_NAME.trim();
247+
if (tagName.length) {
248+
await github.rest.git.createRef({
249+
owner,
250+
repo,
251+
ref: `refs/tags/${tagName}`,
252+
sha: newCommit.sha
253+
});
254+
core.info(`Created tag ${tagName} at ${newCommit.sha}`);
255+
}
256+
202257
core.setOutput('commit_sha', newCommit.sha);

0 commit comments

Comments
 (0)