Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ jobs:

test-lock-branch:
uses: ./.github/workflows/test-lock-branch.yml

test-create-pull-request:
uses: ./.github/workflows/test-create-pull-request.yml
186 changes: 186 additions & 0 deletions .github/workflows/test-create-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: Test Create Pull Request Action

on:
workflow_call:
pull_request:
paths:
- 'create-pull-request/**'
- '.github/workflows/test-create-pull-request.yml'
push:
branches:
- branch-*
paths:
- 'create-pull-request/**'
- '.github/workflows/test-create-pull-request.yml'
workflow_dispatch:

jobs:
schema-validation:
name: Schema Validation
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Verify composite action type
run: |
set -euo pipefail
if grep -q 'using: .composite.' create-pull-request/action.yml; then
echo "PASS: using: composite found"
else
echo "FAIL: using: composite not found"
exit 1
fi

- name: Verify expected inputs exist
run: |
set -euo pipefail
INPUTS=(
token commit-message committer author signoff
branch branch-suffix base title body body-path
labels assignees reviewers team-reviewers milestone
draft delete-branch add-paths maintainer-can-modify
)
for input in "${INPUTS[@]}"; do
if grep -q "^ ${input}:" create-pull-request/action.yml; then
echo "PASS: input '${input}' found"
else
echo "FAIL: input '${input}' not found"
exit 1
fi
done

- name: Verify expected outputs exist
run: |
set -euo pipefail
OUTPUTS=(
pull-request-number pull-request-url
pull-request-operation pull-request-head-sha
pull-request-branch
)
for output in "${OUTPUTS[@]}"; do
if grep -q "^ ${output}:" create-pull-request/action.yml; then
echo "PASS: output '${output}' found"
else
echo "FAIL: output '${output}' not found"
exit 1
fi
done

- name: Verify bash steps use set -euo pipefail
run: |
set -euo pipefail
# Count bash steps (shell: bash) and verify they use set -euo pipefail
BASH_STEPS=$(grep -c 'shell: bash' create-pull-request/action.yml)
PIPEFAIL_COUNT=$(grep -c 'set -euo pipefail' create-pull-request/action.yml)
echo "Bash steps: ${BASH_STEPS}, pipefail occurrences: ${PIPEFAIL_COUNT}"
if [ "$PIPEFAIL_COUNT" -ge "$BASH_STEPS" ]; then
echo "PASS: all bash steps use set -euo pipefail"
else
echo "FAIL: not all bash steps use set -euo pipefail"
exit 1
fi

- name: Verify vault step has continue-on-error
run: |
set -euo pipefail
if grep -B5 -A5 'vault-action-wrapper' create-pull-request/action.yml | grep -q 'continue-on-error: true'; then
echo "PASS: vault step has continue-on-error: true"
else
echo "FAIL: vault step missing continue-on-error: true"
exit 1
fi

- name: Verify inputs are passed via env (no direct interpolation in run blocks)
env:
PATTERN: '\$\{\{ inputs\.'
run: |
set -euo pipefail
# Check that run: blocks do not contain direct input interpolation
in_run_block=false
found_violation=false
while IFS= read -r line; do
# Detect start of run block
if echo "$line" | grep -qE '^\s+run: \|'; then
in_run_block=true
continue
fi
# Detect end of run block (new key at same or lower indent)
if $in_run_block && echo "$line" | grep -qE '^\s{4,6}[a-z]'; then
in_run_block=false
fi
# Check for violation inside run blocks
if $in_run_block && echo "$line" | grep -qE "$PATTERN"; then
# Allow only in comments
if ! echo "$line" | grep -qE '^\s*#'; then
echo "FAIL: direct input interpolation in run block: $line"
found_violation=true
fi
fi
done < create-pull-request/action.yml
if $found_violation; then
exit 1
else
echo "PASS: no direct input interpolation in run blocks"
fi

- name: Summary of Schema Validation
run: |
echo "================================"
echo "Schema Validation Results:"
echo "================================"
echo "All schema checks passed"
echo "================================"

no-changes-test:
name: No Changes Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run action with no file changes
id: create-pr
uses: ./create-pull-request
with:
token: ${{ github.token }}
branch: test/no-changes-${{ github.run_number }}

- name: Verify no-op when no changes
env:
PR_OPERATION: ${{ steps.create-pr.outputs.pull-request-operation }}
run: |
set -euo pipefail
echo "pull-request-operation: ${PR_OPERATION}"
if [ "$PR_OPERATION" = "none" ]; then
echo "PASS: operation is 'none' when no files changed"
else
echo "FAIL: expected operation 'none', got '${PR_OPERATION}'"
exit 1
fi

summary:
name: Test Summary
if: always()
needs: [schema-validation, no-changes-test]
runs-on: ubuntu-latest

steps:
- name: Report results
env:
SCHEMA_RESULT: ${{ needs.schema-validation.result }}
NO_CHANGES_RESULT: ${{ needs.no-changes-test.result }}
run: |
echo "================================"
echo "Test Results Summary:"
echo "================================"
echo "Schema Validation: ${SCHEMA_RESULT}"
echo "No Changes Test: ${NO_CHANGES_RESULT}"
echo "================================"
if [ "$SCHEMA_RESULT" != "success" ] || [ "$NO_CHANGES_RESULT" != "success" ]; then
echo "Some tests failed!"
exit 1
fi
echo "All tests passed!"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A centralized collection of reusable GitHub Actions designed to streamline and a
|--------|-------------|
| [Check Releasability Status](check-releasability-status/README.md) | Checks the releasability status and extracts the version if successful |
| [Create Integration Ticket](create-integration-ticket/README.md) | Creates a Jira integration ticket with a custom summary and links it to another existing ticket |
| [Create Pull Request](create-pull-request/README.md) | Creates or updates a pull request using the `gh` CLI, with vault-based token resolution |
| [Create Jira Release Ticket](create-jira-release-ticket/README.md) | Automates the creation of an "Ask for release" ticket in Jira |
| [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 |
| [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 |
Expand Down
168 changes: 168 additions & 0 deletions create-pull-request/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Create Pull Request Action

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.

## Description

The action:
- Stages and commits file changes on a new branch
- Creates a pull request if one doesn't exist, or updates an existing one
- Supports all common PR options: labels, reviewers, assignees, milestones, drafts
- Automatically resolves authentication tokens via vault, falling back to the provided input token

## Prerequisites

- The repository must be checked out before using this action
- A GitHub token with `contents: write` and `pull-requests: write` permissions
- For vault token resolution: `id-token: write` permission and a vault secret at `development/github/token/{REPO_OWNER_NAME_DASH}-release-automation`

## Inputs

| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `token` | GitHub token (vault token preferred, falls back to this) | No | `${{ github.token }}` |
| `add-paths` | Comma or newline-separated file paths to stage | No | `''` (all changes) |
| `commit-message` | Commit message for changes | No | `[create-pull-request] automated change` |
| `committer` | Committer in `Name <email>` format | No | `github-actions[bot] <...>` |
| `author` | Author in `Name <email>` format | No | `${{ github.actor }} <...>` |
| `signoff` | Add `Signed-off-by` trailer | No | `false` |
| `branch` | PR branch name | No | `create-pull-request/patch` |
| `branch-suffix` | Suffix: `random`, `timestamp`, or `short-commit-hash` | No | `''` |
| `base` | Base branch for PR | No | Current branch |
| `title` | PR title | No | `Changes by create-pull-request action` |
| `body` | PR body | No | `''` |
| `body-path` | File path for PR body content | No | `''` |
| `labels` | Comma or newline-separated labels | No | `''` |
| `assignees` | Comma or newline-separated assignees | No | `''` |
| `reviewers` | Comma or newline-separated reviewers | No | `''` |
| `team-reviewers` | Comma or newline-separated team reviewers | No | `''` |
| `milestone` | Milestone number | No | `''` |
| `draft` | Create as draft PR | No | `false` |
| `delete-branch` | Delete branch after PR is merged | No | `false` |
| `maintainer-can-modify` | Allow maintainer edits | No | `true` |

## Outputs

| Output | Description |
|--------|-------------|
| `pull-request-number` | The number of the created or updated PR |
| `pull-request-url` | The URL of the created or updated PR |
| `pull-request-operation` | The operation performed: `created`, `updated`, or `none` |
| `pull-request-head-sha` | The SHA of the head commit on the PR branch |
| `pull-request-branch` | The name of the PR branch |

## Usage

### Basic usage

```yaml
- uses: actions/checkout@v4

- name: Make changes
run: echo "updated" > file.txt

- name: Create Pull Request
uses: SonarSource/release-github-actions/create-pull-request@v1
with:
title: 'Automated update'
branch: bot/automated-update
```

### With explicit token

```yaml
- name: Create Pull Request
uses: SonarSource/release-github-actions/create-pull-request@v1
with:
token: ${{ secrets.MY_TOKEN }}
commit-message: 'Update dependencies'
title: 'Update dependencies'
branch: bot/update-deps
```

### With labels and reviewers

```yaml
- name: Create Pull Request
uses: SonarSource/release-github-actions/create-pull-request@v1
with:
title: 'Update rule metadata'
branch: bot/update-rule-metadata
branch-suffix: timestamp
labels: skip-qa
reviewers: user1,user2
team-reviewers: team-a
```

### With draft PR

```yaml
- name: Create Pull Request
uses: SonarSource/release-github-actions/create-pull-request@v1
with:
title: 'WIP: New feature'
branch: bot/new-feature
draft: true
body: |
## Summary
This PR adds a new feature.

## Details
- Change 1
- Change 2
```

### With branch suffix

```yaml
- name: Create Pull Request
uses: SonarSource/release-github-actions/create-pull-request@v1
with:
title: 'Automated changes'
branch: bot/changes
branch-suffix: timestamp # or: random, short-commit-hash
```

## Token Resolution

The action resolves the GitHub token using the following priority:

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`
2. **Input token** (fallback): Uses the `token` input (defaults to `${{ github.token }}`)

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.

## Migration from peter-evans/create-pull-request

This action provides a compatible interface. Key differences:

| Feature | peter-evans/create-pull-request | This action |
|---------|--------------------------------|-------------|
| Runtime | Node.js | Bash + `gh` CLI |
| Token | Input only | Vault-preferred, input fallback |
| Push | Built-in | `git push --force-with-lease` |
| PR create/update | GitHub API | `gh pr create` / `gh pr edit` |

To migrate, replace the `uses:` reference and ensure inputs match. Most inputs are compatible by name and behavior.

## Behavior

### No changes detected
When no files have changed, the action outputs `pull-request-operation=none` and exits successfully without creating a branch or PR.

### Existing PR
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.

### Branch management
- The action uses `git checkout -B` to create or reset the PR branch
- Push uses `--force-with-lease` to safely update the remote branch
- When `delete-branch: true`, the branch is deleted only after the PR is merged

## Error Handling

The action will fail if:
- No valid token is available (vault and input both empty)
- The committer format is invalid
- An invalid `branch-suffix` value is provided
- Git operations fail (commit, push)
- PR creation or update fails
Loading
Loading