Skip to content

Commit 3cb5b26

Browse files
feat: add context documentation and implement API throttling for pull request handling
1 parent 84b1d32 commit 3cb5b26

File tree

6 files changed

+163
-10
lines changed

6 files changed

+163
-10
lines changed

docs/context.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Project Context Overview
2+
3+
This document captures the current state of the **CPython Patch PR Action** repository, the key
4+
capabilities that have been implemented so far, and any manual configuration that enables the
5+
nightly workflows to run end-to-end.
6+
7+
---
8+
9+
## Action capabilities
10+
11+
- **Version detection**
12+
- GitHub tag fetcher with python.org fallback.
13+
- Runner availability validation against `actions/python-versions`.
14+
- Pre-release guard (opt-in via `include_prerelease`).
15+
- Track alignment + idempotence helpers.
16+
- **Scanning & rewriting**
17+
- Globs for common Python version files.
18+
- Regex matchers covering workflows, Dockerfiles, runtime files, `pyproject.toml`, `Pipfile`,
19+
Conda `environment.yml`, etc.
20+
- Dry-run summaries and targeted patch computation that preserves Docker suffixes.
21+
- **Git / PR integration**
22+
- Branch+commit helper (`chore/bump-python-<track>`).
23+
- Pull-request helper with Octokit throttling & duplicate-PR prevention.
24+
- **Testing & coverage**
25+
- Vitest suite with >90% coverage (run `npm run test -- --coverage`).
26+
- Git-based integration tests using temporary repositories.
27+
- **Documentation & metadata**
28+
- README with SEO-friendly quick start and advanced configuration.
29+
- CHANGELOG (Keep a Changelog format).
30+
- SECURITY policy outlining required permissions and endpoints.
31+
32+
---
33+
34+
## GitHub workflows
35+
36+
### `.github/workflows/fixtures.yml`
37+
Runs the action in **dry-run** mode against local fixtures:
38+
39+
- CI jobs: install, build (`npm run build`), bundle (`npm run bundle`), then `uses: ./` with
40+
`dry_run: true`.
41+
- Fixture matrix (`fixtures/basic`, `fixtures/workflow`) ensures core scanners stay stable.
42+
- No secrets required (read-only `GITHUB_TOKEN` is sufficient).
43+
44+
### `.github/workflows/nightly.yml`
45+
Weekly **Monday 03:00 UTC** E2E run against a sandbox repository:
46+
47+
- Builds/bundles this repo.
48+
- Clones the sandbox repo and executes the action in non-dry-run mode (`dry_run: false`).
49+
- Writes a summary containing repository, branch, track, automerge flag, files changed,
50+
and `skipped_reason`.
51+
- Requires secrets and optional variables (see next section).
52+
53+
---
54+
55+
## Sandbox configuration
56+
57+
Set the following in **Settings → Secrets and variables → Actions**:
58+
59+
| Name | Type | Required | Description |
60+
|------|------|----------|-------------|
61+
| `SANDBOX_REPO` | Secret || Repo slug e.g. `username/python-sandbox`. |
62+
| `SANDBOX_TOKEN` | Secret || PAT with `contents: read/write` and `pull-requests: read/write` scopes for the sandbox repo. |
63+
| `SANDBOX_TRACK` | Variable | Optional (`3.11` default) | CPython minor to track. |
64+
| `SANDBOX_DEFAULT_BRANCH` | Variable | Optional (`main` default) | Branch to check out before running. |
65+
| `SANDBOX_AUTOMERGE` | Variable | Optional (`false` default) | Pass-through to the action’s `automerge` input. |
66+
67+
### Sandbox repo content
68+
69+
Include deliberate CPython version pins (e.g. Dockerfile, `.github/workflows/*.yml`,
70+
`runtime.txt`, `pyproject.toml`, `Pipfile`, `environment.yml`). A sample prompt for scaffolding
71+
the sandbox is captured in the main discussion and can be reused to populate example files.
72+
73+
---
74+
75+
## Running locally
76+
77+
```bash
78+
npm ci
79+
npm run lint
80+
npm run test -- --coverage
81+
npm run bundle
82+
node dist/index.js # Placeholder run output
83+
```
84+
85+
Use `node dist/index.js` to confirm the bundled action executes; currently it only logs
86+
configuration and emits `skipped_reason: not_implemented`.
87+
88+
---
89+
90+
## Roadmap snapshot (docs/tasks.md)
91+
92+
Tasks 1–31 are marked complete, covering scaffolding, metadata, docs, scanning engine,
93+
rewrite logic, branch/PR automation, dry-run workflows, sandbox nightly job, and Octokit
94+
throttling. Task 32 onward focuses on bundling `dist/`, release workflows, CodeQL,
95+
example consumer repos, and additional guardrails.
96+
97+
Refer to `docs/tasks.md` for the full list of upcoming work.
98+
99+
---
100+
101+
_Last updated: 2025-10-09_

docs/tasks.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,14 @@ Use Context7 MCP for up to date documentation.
194194
Verify: Snapshot outputs stable.
195195

196196
29. [x] **Dry-run CI job on fixtures**
197-
Upload `GITHUB_STEP_SUMMARY` artifacts.
198-
Verify: Artifacts contain expected diffs.
197+
Upload `GITHUB_STEP_SUMMARY` artifacts.
198+
Verify: Artifacts contain expected diffs.
199199

200200
30. [x] **E2E sandbox nightly**
201201
Nightly scheduled PR cycle in a sandbox repo.
202202
Verify: PR created and closes as expected.
203203

204-
31. [ ] **API throttling**
204+
31. [x] **API throttling**
205205
Use Octokit throttling plugin. Retry with backoff.
206206
Verify: Tests assert retries and clear messages.
207207

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"homepage": "https://github.com/CasperKristiansson/python-version-patch-pr#readme",
4040
"dependencies": {
4141
"@actions/core": "^1.10.1",
42+
"@octokit/plugin-throttling": "^11.0.2",
4243
"@octokit/rest": "^22.0.0",
4344
"cheerio": "^1.1.2",
4445
"fast-glob": "^3.3.3",

src/git/pull-request.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { Octokit } from '@octokit/rest';
2+
import { throttling } from '@octokit/plugin-throttling';
3+
4+
const ThrottledOctokit = Octokit.plugin(throttling);
25

36
export interface OctokitClient {
47
pulls: {
@@ -52,7 +55,14 @@ export interface PullRequestResult {
5255
const USER_AGENT = 'python-version-patch-pr/0.1.0';
5356

5457
function createClient(authToken: string): OctokitClient {
55-
return new Octokit({ auth: authToken, userAgent: USER_AGENT });
58+
return new ThrottledOctokit({
59+
auth: authToken,
60+
userAgent: USER_AGENT,
61+
throttle: {
62+
onRateLimit: (_retryAfter, options) => options.request.retryCount === 0,
63+
onSecondaryRateLimit: () => true,
64+
},
65+
});
5666
}
5767

5868
export async function createOrUpdatePullRequest(

tests/git-pull-request.test.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@ const octokitModule = vi.hoisted(() => {
44
const list = vi.fn();
55
const create = vi.fn();
66
const update = vi.fn();
7-
const Octokit = vi.fn().mockImplementation(() => ({ pulls: { list, create, update } }));
8-
return { Octokit, list, create, update };
7+
const OctokitBase = vi
8+
.fn()
9+
.mockImplementation((options: { throttle?: Record<string, unknown> }) => {
10+
if (options.throttle && typeof options.throttle.onRateLimit === 'function') {
11+
options.throttle.onRateLimit(1, { method: 'GET', url: 'test', request: { retryCount: 0 } });
12+
}
13+
if (options.throttle && typeof options.throttle.onSecondaryRateLimit === 'function') {
14+
options.throttle.onSecondaryRateLimit(1, { method: 'GET', url: 'test' });
15+
}
16+
return { pulls: { list, create, update } };
17+
});
18+
const plugin = vi.fn(() => OctokitBase);
19+
Object.assign(OctokitBase, { plugin });
20+
return { Octokit: OctokitBase, list, create, update, plugin };
921
});
1022

1123
vi.mock('@octokit/rest', () => ({
@@ -121,10 +133,16 @@ describe('createOrUpdatePullRequest', () => {
121133

122134
const result = await createOrUpdatePullRequest(baseOptions);
123135

124-
expect(octokitModule.Octokit).toHaveBeenCalledWith({
125-
auth: 'token',
126-
userAgent: 'python-version-patch-pr/0.1.0',
127-
});
136+
expect(octokitModule.Octokit).toHaveBeenCalledWith(
137+
expect.objectContaining({
138+
auth: 'token',
139+
userAgent: 'python-version-patch-pr/0.1.0',
140+
throttle: expect.objectContaining({
141+
onRateLimit: expect.any(Function),
142+
onSecondaryRateLimit: expect.any(Function),
143+
}),
144+
}),
145+
);
128146
expect(result).toEqual({ action: 'created', number: 5, url: undefined });
129147
});
130148
});

0 commit comments

Comments
 (0)