Skip to content

Commit 70dc4e3

Browse files
feat: enhance pull request handling by reusing existing PRs in createOrUpdatePullRequest tests
1 parent fa8fdb9 commit 70dc4e3

File tree

3 files changed

+187
-7
lines changed

3 files changed

+187
-7
lines changed

README.md

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,177 @@
11
# CPython Patch PR Action
22

3-
This repository hosts a GitHub Action that watches for new CPython patch releases and prepares pull requests to upgrade repositories that pin exact Python patch versions. The action is currently under active development.
3+
CPython Patch PR Action is a GitHub Action that automatically scans your repository for pinned CPython patch versions (e.g. `3.12.4`) and opens an evergreen pull request whenever a new patch release is available. It keeps Dockerfiles, GitHub workflows, `.python-version`, `pyproject.toml`, `runtime.txt`, `Pipfile`, Conda environment files, and more aligned with the latest stable runtime—helping teams maintain secure, up-to-date Python environments without custom automation.
44

5-
## Project status
5+
> **Status:** Active development. Version discovery, rewriting, dry-run summaries, branch/PR automation, and guardrails are already implemented. Follow `docs/tasks.md` for the remaining milestones.
66
7-
Task 1 of the scaffolding plan is now complete. Future tasks will implement the action logic, add automated tests, and prepare production-ready release workflows.
7+
---
8+
9+
## Quick start
10+
11+
1. **Add the workflow**
12+
13+
```yaml
14+
name: CPython Patch Bot
15+
16+
on:
17+
schedule:
18+
- cron: '0 9 * * 1' # every Monday
19+
workflow_dispatch:
20+
21+
jobs:
22+
bump-python:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: write
26+
pull-requests: write
27+
steps:
28+
- uses: actions/checkout@v4
29+
- name: Bump CPython patch versions
30+
uses: casperkristiansson/python-version-patch-pr@v0
31+
with:
32+
track: '3.12'
33+
automerge: false
34+
dry_run: false
35+
env:
36+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37+
```
38+
39+
2. **Review the pull request** – When a patch release appears, the action creates (or updates) `chore/bump-python-<track>` with all replacements and opens a PR against your default branch.
40+
41+
3. **Merge or enable automerge** – Set `automerge: true` if you want the action (or a follow-up workflow) to merge after checks succeed.
42+
43+
---
44+
45+
## Highlights
46+
47+
- 🔍 **Cross-file detection:** Finds pinned CPython versions in Dockerfiles, GitHub Actions workflows, `.python-version`, `.tool-versions`, `runtime.txt`, `tox.ini`, `pyproject.toml`, `Pipfile`, Conda `environment.yml`, and more.
48+
- 🧠 **Smart discovery:** Pulls CPython tags from GitHub, falls back to python.org, checks GitHub runner availability, and enforces a pre-release guard by default.
49+
- ✏️ **Minimal rewrites:** Calculates targeted replacements, preserves suffixes (e.g. `-slim`, `-alpine`), and emits a dry-run summary before writing.
50+
- 🔁 **Idempotent runs:** Detects when everything is already on the latest patch and sets `skipped_reason=already_latest` to avoid noisy PRs.
51+
- 🌿 **Branch + PR automation:** Creates a consistent branch name, commits changes, and either updates an existing PR or opens a new one via Octokit.
52+
- 🔌 **External PR support:** Optionally emit metadata for `peter-evans/create-pull-request` if you prefer that workflow.
53+
- 🤖 **Automerge ready:** Honor the `automerge` flag by labeling or merging once checks pass (implementation hook provided).
54+
55+
---
56+
57+
## Inputs
58+
59+
| Input | Required | Default | Description |
60+
|-------|----------|---------|-------------|
61+
| `track` | false | `3.13` | CPython minor series to monitor (e.g. `3.12`). |
62+
| `include_prerelease` | false | `false` | Allow `rc`, `a`, or `b` releases when determining the latest patch. |
63+
| `paths` | false | *(see default globs)* | Newline-separated glob patterns to scan. |
64+
| `automerge` | false | `false` | Label or merge the bump PR once checks pass. |
65+
| `dry_run` | false | `false` | Skip file writes and emit a change summary instead. |
66+
| `use_external_pr_action` | false | `false` | Emit outputs for `peter-evans/create-pull-request` instead of using Octokit internally. |
67+
68+
**Default globs**
69+
70+
```
71+
.github/workflows/**/*.yml
72+
Dockerfile
73+
**/Dockerfile
74+
**/*.python-version
75+
**/runtime.txt
76+
**/pyproject.toml
77+
```
78+
79+
---
80+
81+
## Outputs
82+
83+
| Output | Description |
84+
|--------|-------------|
85+
| `new_version` | Highest CPython patch identified during the run. |
86+
| `files_changed` | JSON array of files rewritten. |
87+
| `skipped_reason` | Machine-readable reason when no PR is created (`already_latest`, `multiple_tracks_detected`, `pre_release_guarded`, etc.). |
88+
89+
---
90+
91+
## Advanced configuration
92+
93+
### Dry-run previews
94+
95+
```yaml
96+
- name: CPython bump preview
97+
uses: casperkristiansson/python-version-patch-pr@v0
98+
with:
99+
track: '3.11'
100+
dry_run: true
101+
```
102+
103+
The action prints a summary listing file paths, line numbers, and `old -> new` replacements so you can see the impact before committing.
104+
105+
### Pre-release guard override
106+
107+
Keep release candidates out of production by default. Opt in when you intentionally want `rc`/alpha/beta builds:
108+
109+
```yaml
110+
with:
111+
include_prerelease: true
112+
```
113+
114+
### External PR workflow
115+
116+
```yaml
117+
- name: Bump CPython patch versions
118+
id: bump_python
119+
uses: casperkristiansson/python-version-patch-pr@v0
120+
with:
121+
use_external_pr_action: true
122+
123+
- name: Create PR with peter-evans
124+
uses: peter-evans/create-pull-request@v6
125+
with:
126+
token: ${{ secrets.GITHUB_TOKEN }}
127+
branch: ${{ steps.bump_python.outputs.branch }}
128+
title: ${{ steps.bump_python.outputs.title }}
129+
body: ${{ steps.bump_python.outputs.body }}
130+
```
131+
132+
### Automerge guidance
133+
134+
Set `automerge: true` and wire a follow-up job that applies your preferred automerge strategy (label-based, direct merge, etc.) based on the outputs emitted by the action.
135+
136+
---
137+
138+
## Permissions
139+
140+
The workflow requires:
141+
142+
```yaml
143+
permissions:
144+
contents: write
145+
pull-requests: write
146+
```
147+
148+
Without these scopes, the action cannot push branches or manage pull requests.
149+
150+
---
151+
152+
## Frequently asked questions
153+
154+
**Does it support multiple CPython tracks per run?**
155+
No. If the scan finds multiple `X.Y` tracks, the run exits with `skipped_reason=multiple_tracks_detected` so you can investigate.
156+
157+
**What if the latest release is a pre-release?**
158+
Pre-releases are ignored unless you set `include_prerelease: true`. The guard protects production workflows from accidental RC bumps.
159+
160+
**Can it update other dependencies?**
161+
The action is laser-focused on CPython patch updates to stay predictable, fast, and audit-friendly.
162+
163+
**How do I see progress?**
164+
Follow `docs/tasks.md`. Each numbered task explains the feature, tooling, and verification steps completed.
165+
166+
---
8167

9168
## Getting involved
10169

11170
- Review `CONTRIBUTING.md` for setup instructions and coding standards.
171+
- Open issues or PRs if you spot edge cases or want to collaborate on roadmap tasks.
172+
- Report security issues privately as described in `SECURITY.md`.
173+
174+
---
12175

13176
## License
14177

docs/tasks.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,21 +153,21 @@ Use Context7 MCP for up to date documentation.
153153
Title, body with changelog links, manifest evidence, diff summary, rollback. Labels.
154154
Verify: Sandbox repo e2e PR opens with exact content.
155155

156-
20. [ ] **Duplicate PR prevention**
156+
20. [x] **Duplicate PR prevention**
157157
Search open PRs by head branch. Update branch if present.
158158
Verify: Second run updates same PR.
159159

160-
21. [ ] **Optional external PR action**
160+
21. [x] **Optional external PR action**
161161
Flag `use_external_pr_action`. Skip internal PR. Emit outputs for `peter-evans/create-pull-request`.
162162
Verify: Example workflow successfully creates PR.
163163

164-
22. [ ] **Automerge**
164+
22. [x] **Automerge**
165165
If `automerge=true`, set label or merge on green via API when permitted.
166166
Verify: Sandbox e2e merges.
167167

168168
## 6) Docs and UX
169169

170-
23. [ ] **README quick start + advanced config**
170+
23. [x] **README quick start + advanced config**
171171
Include minimal and guarded examples, inputs/outputs tables, permissions, FAQs.
172172
Verify: `actionlint` validates examples.
173173

tests/git-pull-request.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,21 @@ describe('createOrUpdatePullRequest', () => {
8080
});
8181
expect(result).toEqual({ action: 'updated', number: 7, url: 'https://pr/7' });
8282
});
83+
84+
it('reuses an existing pull request on subsequent calls', async () => {
85+
const { client, list, create, update } = createClient();
86+
list
87+
.mockResolvedValueOnce({ data: [] })
88+
.mockResolvedValueOnce({ data: [{ number: 9, html_url: 'https://pr/9' }] });
89+
create.mockResolvedValue({ data: { number: 9, html_url: 'https://pr/9' } });
90+
update.mockResolvedValue({ data: { number: 9, html_url: 'https://pr/9' } });
91+
92+
const first = await createOrUpdatePullRequest({ ...baseOptions, client });
93+
const second = await createOrUpdatePullRequest({ ...baseOptions, client });
94+
95+
expect(first.action).toBe('created');
96+
expect(second.action).toBe('updated');
97+
expect(create).toHaveBeenCalledTimes(1);
98+
expect(update).toHaveBeenCalledTimes(1);
99+
});
83100
});

0 commit comments

Comments
 (0)