Skip to content

Commit 00d1860

Browse files
GHA-187 Add Action to create a PR to replace 3rd-party action (#94)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent de6d6b8 commit 00d1860

File tree

5 files changed

+793
-0
lines changed

5 files changed

+793
-0
lines changed

.github/workflows/test-all.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,6 @@ jobs:
4747

4848
test-lock-branch:
4949
uses: ./.github/workflows/test-lock-branch.yml
50+
51+
test-create-pull-request:
52+
uses: ./.github/workflows/test-create-pull-request.yml
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
name: Test Create Pull Request Action
2+
3+
on:
4+
workflow_call:
5+
pull_request:
6+
paths:
7+
- 'create-pull-request/**'
8+
- '.github/workflows/test-create-pull-request.yml'
9+
push:
10+
branches:
11+
- branch-*
12+
paths:
13+
- 'create-pull-request/**'
14+
- '.github/workflows/test-create-pull-request.yml'
15+
workflow_dispatch:
16+
17+
jobs:
18+
schema-validation:
19+
name: Schema Validation
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v4
25+
26+
- name: Verify composite action type
27+
run: |
28+
set -euo pipefail
29+
if grep -q 'using: .composite.' create-pull-request/action.yml; then
30+
echo "PASS: using: composite found"
31+
else
32+
echo "FAIL: using: composite not found"
33+
exit 1
34+
fi
35+
36+
- name: Verify expected inputs exist
37+
run: |
38+
set -euo pipefail
39+
INPUTS=(
40+
token commit-message committer author signoff
41+
branch branch-suffix base title body body-path
42+
labels assignees reviewers team-reviewers milestone
43+
draft delete-branch add-paths maintainer-can-modify
44+
)
45+
for input in "${INPUTS[@]}"; do
46+
if grep -q "^ ${input}:" create-pull-request/action.yml; then
47+
echo "PASS: input '${input}' found"
48+
else
49+
echo "FAIL: input '${input}' not found"
50+
exit 1
51+
fi
52+
done
53+
54+
- name: Verify expected outputs exist
55+
run: |
56+
set -euo pipefail
57+
OUTPUTS=(
58+
pull-request-number pull-request-url
59+
pull-request-operation pull-request-head-sha
60+
pull-request-branch
61+
)
62+
for output in "${OUTPUTS[@]}"; do
63+
if grep -q "^ ${output}:" create-pull-request/action.yml; then
64+
echo "PASS: output '${output}' found"
65+
else
66+
echo "FAIL: output '${output}' not found"
67+
exit 1
68+
fi
69+
done
70+
71+
- name: Verify bash steps use set -euo pipefail
72+
run: |
73+
set -euo pipefail
74+
# Count bash steps (shell: bash) and verify they use set -euo pipefail
75+
BASH_STEPS=$(grep -c 'shell: bash' create-pull-request/action.yml)
76+
PIPEFAIL_COUNT=$(grep -c 'set -euo pipefail' create-pull-request/action.yml)
77+
echo "Bash steps: ${BASH_STEPS}, pipefail occurrences: ${PIPEFAIL_COUNT}"
78+
if [ "$PIPEFAIL_COUNT" -ge "$BASH_STEPS" ]; then
79+
echo "PASS: all bash steps use set -euo pipefail"
80+
else
81+
echo "FAIL: not all bash steps use set -euo pipefail"
82+
exit 1
83+
fi
84+
85+
- name: Verify vault step has continue-on-error
86+
run: |
87+
set -euo pipefail
88+
if grep -B5 -A5 'vault-action-wrapper' create-pull-request/action.yml | grep -q 'continue-on-error: true'; then
89+
echo "PASS: vault step has continue-on-error: true"
90+
else
91+
echo "FAIL: vault step missing continue-on-error: true"
92+
exit 1
93+
fi
94+
95+
- name: Verify inputs are passed via env (no direct interpolation in run blocks)
96+
env:
97+
PATTERN: '\$\{\{ inputs\.'
98+
run: |
99+
set -euo pipefail
100+
# Check that run: blocks do not contain direct input interpolation
101+
in_run_block=false
102+
found_violation=false
103+
while IFS= read -r line; do
104+
# Detect start of run block
105+
if echo "$line" | grep -qE '^\s+run: \|'; then
106+
in_run_block=true
107+
continue
108+
fi
109+
# Detect end of run block (new key at same or lower indent)
110+
if $in_run_block && echo "$line" | grep -qE '^\s{4,6}[a-z]'; then
111+
in_run_block=false
112+
fi
113+
# Check for violation inside run blocks
114+
if $in_run_block && echo "$line" | grep -qE "$PATTERN"; then
115+
# Allow only in comments
116+
if ! echo "$line" | grep -qE '^\s*#'; then
117+
echo "FAIL: direct input interpolation in run block: $line"
118+
found_violation=true
119+
fi
120+
fi
121+
done < create-pull-request/action.yml
122+
if $found_violation; then
123+
exit 1
124+
else
125+
echo "PASS: no direct input interpolation in run blocks"
126+
fi
127+
128+
- name: Summary of Schema Validation
129+
run: |
130+
echo "================================"
131+
echo "Schema Validation Results:"
132+
echo "================================"
133+
echo "All schema checks passed"
134+
echo "================================"
135+
136+
no-changes-test:
137+
name: No Changes Test
138+
runs-on: ubuntu-latest
139+
140+
steps:
141+
- name: Checkout code
142+
uses: actions/checkout@v4
143+
144+
- name: Run action with no file changes
145+
id: create-pr
146+
uses: ./create-pull-request
147+
with:
148+
token: ${{ github.token }}
149+
branch: test/no-changes-${{ github.run_number }}
150+
151+
- name: Verify no-op when no changes
152+
env:
153+
PR_OPERATION: ${{ steps.create-pr.outputs.pull-request-operation }}
154+
run: |
155+
set -euo pipefail
156+
echo "pull-request-operation: ${PR_OPERATION}"
157+
if [ "$PR_OPERATION" = "none" ]; then
158+
echo "PASS: operation is 'none' when no files changed"
159+
else
160+
echo "FAIL: expected operation 'none', got '${PR_OPERATION}'"
161+
exit 1
162+
fi
163+
164+
summary:
165+
name: Test Summary
166+
if: always()
167+
needs: [schema-validation, no-changes-test]
168+
runs-on: ubuntu-latest
169+
170+
steps:
171+
- name: Report results
172+
env:
173+
SCHEMA_RESULT: ${{ needs.schema-validation.result }}
174+
NO_CHANGES_RESULT: ${{ needs.no-changes-test.result }}
175+
run: |
176+
echo "================================"
177+
echo "Test Results Summary:"
178+
echo "================================"
179+
echo "Schema Validation: ${SCHEMA_RESULT}"
180+
echo "No Changes Test: ${NO_CHANGES_RESULT}"
181+
echo "================================"
182+
if [ "$SCHEMA_RESULT" != "success" ] || [ "$NO_CHANGES_RESULT" != "success" ]; then
183+
echo "Some tests failed!"
184+
exit 1
185+
fi
186+
echo "All tests passed!"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A centralized collection of reusable GitHub Actions designed to streamline and a
88
|--------|-------------|
99
| [Check Releasability Status](check-releasability-status/README.md) | Checks the releasability status and extracts the version if successful |
1010
| [Create Integration Ticket](create-integration-ticket/README.md) | Creates a Jira integration ticket with a custom summary and links it to another existing ticket |
11+
| [Create Pull Request](create-pull-request/README.md) | Creates or updates a pull request using the `gh` CLI, with vault-based token resolution |
1112
| [Create Jira Release Ticket](create-jira-release-ticket/README.md) | Automates the creation of an "Ask for release" ticket in Jira |
1213
| [Create Jira Version](create-jira-version/README.md) | Creates a new version in a Jira project, with the ability to automatically determine the next version number |
1314
| [Get Jira Release Notes](get-jira-release-notes/README.md) | Fetches Jira release notes and generates the release notes URL for a given project and version |

create-pull-request/README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Create Pull Request Action
2+
3+
This GitHub Action creates or updates a pull request using the `gh` CLI. It is designed as an in-house replacement for `peter-evans/create-pull-request`, with integrated vault-based token resolution.
4+
5+
## Description
6+
7+
The action:
8+
- Stages and commits file changes on a new branch
9+
- Creates a pull request if one doesn't exist, or updates an existing one
10+
- Supports all common PR options: labels, reviewers, assignees, milestones, drafts
11+
- Automatically resolves authentication tokens via vault, falling back to the provided input token
12+
13+
## Prerequisites
14+
15+
- The repository must be checked out before using this action
16+
- A GitHub token with `contents: write` and `pull-requests: write` permissions
17+
- For vault token resolution: `id-token: write` permission and a vault secret at `development/github/token/{REPO_OWNER_NAME_DASH}-release-automation`
18+
19+
## Inputs
20+
21+
| Input | Description | Required | Default |
22+
|-------|-------------|----------|---------|
23+
| `token` | GitHub token (vault token preferred, falls back to this) | No | `${{ github.token }}` |
24+
| `add-paths` | Comma or newline-separated file paths to stage | No | `''` (all changes) |
25+
| `commit-message` | Commit message for changes | No | `[create-pull-request] automated change` |
26+
| `committer` | Committer in `Name <email>` format | No | `github-actions[bot] <...>` |
27+
| `author` | Author in `Name <email>` format | No | `${{ github.actor }} <...>` |
28+
| `signoff` | Add `Signed-off-by` trailer | No | `false` |
29+
| `branch` | PR branch name | No | `create-pull-request/patch` |
30+
| `branch-suffix` | Suffix: `random`, `timestamp`, or `short-commit-hash` | No | `''` |
31+
| `base` | Base branch for PR | No | Current branch |
32+
| `title` | PR title | No | `Changes by create-pull-request action` |
33+
| `body` | PR body | No | `''` |
34+
| `body-path` | File path for PR body content | No | `''` |
35+
| `labels` | Comma or newline-separated labels | No | `''` |
36+
| `assignees` | Comma or newline-separated assignees | No | `''` |
37+
| `reviewers` | Comma or newline-separated reviewers | No | `''` |
38+
| `team-reviewers` | Comma or newline-separated team reviewers | No | `''` |
39+
| `milestone` | Milestone number | No | `''` |
40+
| `draft` | Create as draft PR | No | `false` |
41+
| `delete-branch` | Delete branch after PR is merged | No | `false` |
42+
| `maintainer-can-modify` | Allow maintainer edits | No | `true` |
43+
44+
## Outputs
45+
46+
| Output | Description |
47+
|--------|-------------|
48+
| `pull-request-number` | The number of the created or updated PR |
49+
| `pull-request-url` | The URL of the created or updated PR |
50+
| `pull-request-operation` | The operation performed: `created`, `updated`, or `none` |
51+
| `pull-request-head-sha` | The SHA of the head commit on the PR branch |
52+
| `pull-request-branch` | The name of the PR branch |
53+
54+
## Usage
55+
56+
### Basic usage
57+
58+
```yaml
59+
- uses: actions/checkout@v4
60+
61+
- name: Make changes
62+
run: echo "updated" > file.txt
63+
64+
- name: Create Pull Request
65+
uses: SonarSource/release-github-actions/create-pull-request@v1
66+
with:
67+
title: 'Automated update'
68+
branch: bot/automated-update
69+
```
70+
71+
### With explicit token
72+
73+
```yaml
74+
- name: Create Pull Request
75+
uses: SonarSource/release-github-actions/create-pull-request@v1
76+
with:
77+
token: ${{ secrets.MY_TOKEN }}
78+
commit-message: 'Update dependencies'
79+
title: 'Update dependencies'
80+
branch: bot/update-deps
81+
```
82+
83+
### With labels and reviewers
84+
85+
```yaml
86+
- name: Create Pull Request
87+
uses: SonarSource/release-github-actions/create-pull-request@v1
88+
with:
89+
title: 'Update rule metadata'
90+
branch: bot/update-rule-metadata
91+
branch-suffix: timestamp
92+
labels: skip-qa
93+
reviewers: user1,user2
94+
team-reviewers: team-a
95+
```
96+
97+
### With draft PR
98+
99+
```yaml
100+
- name: Create Pull Request
101+
uses: SonarSource/release-github-actions/create-pull-request@v1
102+
with:
103+
title: 'WIP: New feature'
104+
branch: bot/new-feature
105+
draft: true
106+
body: |
107+
## Summary
108+
This PR adds a new feature.
109+
110+
## Details
111+
- Change 1
112+
- Change 2
113+
```
114+
115+
### With branch suffix
116+
117+
```yaml
118+
- name: Create Pull Request
119+
uses: SonarSource/release-github-actions/create-pull-request@v1
120+
with:
121+
title: 'Automated changes'
122+
branch: bot/changes
123+
branch-suffix: timestamp # or: random, short-commit-hash
124+
```
125+
126+
## Token Resolution
127+
128+
The action resolves the GitHub token using the following priority:
129+
130+
1. **Vault token** (preferred): Fetches `development/github/token/{REPO_OWNER_NAME_DASH}-release-automation` via `SonarSource/vault-action-wrapper@v3` with `continue-on-error: true`
131+
2. **Input token** (fallback): Uses the `token` input (defaults to `${{ github.token }}`)
132+
133+
If both fail, the action errors. This design allows the action to work in repositories with vault access (using a more privileged token) while gracefully falling back to the workflow token.
134+
135+
## Migration from peter-evans/create-pull-request
136+
137+
This action provides a compatible interface. Key differences:
138+
139+
| Feature | peter-evans/create-pull-request | This action |
140+
|---------|--------------------------------|-------------|
141+
| Runtime | Node.js | Bash + `gh` CLI |
142+
| Token | Input only | Vault-preferred, input fallback |
143+
| Push | Built-in | `git push --force-with-lease` |
144+
| PR create/update | GitHub API | `gh pr create` / `gh pr edit` |
145+
146+
To migrate, replace the `uses:` reference and ensure inputs match. Most inputs are compatible by name and behavior.
147+
148+
## Behavior
149+
150+
### No changes detected
151+
When no files have changed, the action outputs `pull-request-operation=none` and exits successfully without creating a branch or PR.
152+
153+
### Existing PR
154+
When an open PR already exists for the same head and base branch, the action updates it (title, body, labels, reviewers) rather than creating a duplicate.
155+
156+
### Branch management
157+
- The action uses `git checkout -B` to create or reset the PR branch
158+
- Push uses `--force-with-lease` to safely update the remote branch
159+
- When `delete-branch: true`, the branch is deleted only after the PR is merged
160+
161+
## Error Handling
162+
163+
The action will fail if:
164+
- No valid token is available (vault and input both empty)
165+
- The committer format is invalid
166+
- An invalid `branch-suffix` value is provided
167+
- Git operations fail (commit, push)
168+
- PR creation or update fails

0 commit comments

Comments
 (0)