Skip to content

Commit 6704b98

Browse files
committed
docs: make README user focused
1 parent d59e145 commit 6704b98

1 file changed

Lines changed: 157 additions & 111 deletions

File tree

README.md

Lines changed: 157 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,207 @@
11
# GitHub PR Attachment Service
22

3-
A private upload service for agents that need to place files in GitHub pull-request Markdown. It runs on Cloudflare Workers, stores bytes in a private R2 bucket, and returns an opaque public URL plus ready-to-paste Markdown.
3+
Give coding agents a reliable way to add screenshots, logs, PDFs, and other files to GitHub pull requests.
44

5-
The service does not receive GitHub credentials and does not call GitHub. Anyone possessing an attachment URL can retrieve it, so uploads must be nonsensitive.
5+
When an agent cannot use GitHub's browser-only attachment interface, this self-hosted service fills the gap. The agent uploads a local file, receives ready-to-paste Markdown, and adds it to a pull request through the normal GitHub API or CLI.
66

7-
## Agent usage
7+
- Works with Codex, Claude Code, shell scripts, and other automated tools.
8+
- Runs in your own Cloudflare account using a Worker and a private R2 bucket.
9+
- Requires no GitHub credentials and never calls GitHub itself.
10+
- Uses free-tier-conscious storage, request, and retention limits by default.
811

9-
Deploy the service once or configure an existing profile, then expose the included CLI:
12+
Attachment URLs are public to anyone who possesses them. Use the service only for nonsensitive files.
13+
14+
## Demo
15+
16+
[Demo PR #1](https://github.com/none23/github-attachments/pull/1) shows the complete workflow. The images in its description were created on two different machines and uploaded by an agent through this service—there was no manual GitHub attachment step.
17+
18+
## How it works
19+
20+
1. An agent runs the included upload skill or CLI with a local file.
21+
2. Your Cloudflare Worker authenticates the upload and stores the file in private R2.
22+
3. The service returns an opaque public URL and ready-to-paste Markdown.
23+
4. The agent places that Markdown in the pull request body or a comment.
24+
25+
```text
26+
Agent -> your Worker -> private R2
27+
|
28+
GitHub PR <--- Markdown URL
29+
```
30+
31+
## Quick start
32+
33+
You need:
34+
35+
- A Cloudflare account with Workers and R2 available.
36+
- Node.js 24 or later.
37+
- A short-lived Cloudflare API token scoped to your account with:
38+
- Workers Scripts: Edit
39+
- Workers R2 Storage: Edit
40+
- Account Settings: Read
41+
42+
### 1. Deploy your service
43+
44+
```bash
45+
git clone https://github.com/none23/github-attachments.git
46+
cd github-attachments
47+
npm ci
48+
```
49+
50+
Save the Cloudflare deployment credential outside Git:
1051

1152
```bash
1253
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/github-pr-attachments"
1354
mkdir -p "$config_dir"
1455
chmod 700 "$config_dir"
15-
$EDITOR "$config_dir/token"
16-
$EDITOR "$config_dir/url"
17-
chmod 600 "$config_dir/token"
18-
chmod 600 "$config_dir/url"
19-
20-
npm install
21-
npm link
22-
github-attach screenshot.png --alt "Settings after the change"
56+
$EDITOR "$config_dir/deploy.env"
57+
chmod 600 "$config_dir/deploy.env"
2358
```
2459

25-
The `token` file contains only the raw upload token. The `url` file contains the Worker origin, such as `https://github-pr-attachments.example.workers.dev`. Neither file uses variable names or quotes. `npm run setup` creates both files automatically.
60+
`deploy.env` contains:
2661

27-
### Credential resolution
62+
```dotenv
63+
CLOUDFLARE_ACCOUNT_ID=your-account-id
64+
CLOUDFLARE_API_TOKEN=your-short-lived-deployment-token
65+
```
2866

29-
The CLI and bundled agent skill use the first nonempty token in this order:
67+
Create a private bucket and deploy the Worker:
3068

31-
| Priority | Location | Scope |
32-
| ---: | --- | --- |
33-
| 1 | `GITHUB_ATTACHMENTS_TOKEN` in the repository-root `.env` | Current repository override |
34-
| 2 | `GITHUB_ATTACHMENTS_TOKEN` in the process environment | Current process override |
35-
| 3 | `${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/token` | User-level default |
69+
```bash
70+
npm run setup -- \
71+
--worker github-pr-attachments-yourname \
72+
--bucket github-pr-attachments-yourname \
73+
--create-bucket
74+
```
3675

37-
To override the user token for one repository, add this to that repository's `.env`:
76+
Setup creates the bucket, lifecycle rule, Worker secret, and user-level upload profile. Once it succeeds, remove or revoke the Cloudflare deployment token unless this machine also needs to administer the service.
3877

39-
```dotenv
40-
GITHUB_ATTACHMENTS_TOKEN=repository-specific-token
78+
### 2. Install the agent skill and CLI
79+
80+
```bash
81+
mkdir -p "$HOME/.codex/skills" "$HOME/.claude/skills"
82+
ln -s "$PWD/skills/attach-github-pr-files" \
83+
"$HOME/.codex/skills/attach-github-pr-files"
84+
ln -s "$PWD/skills/attach-github-pr-files" \
85+
"$HOME/.claude/skills/attach-github-pr-files"
86+
npm link
4187
```
4288

43-
The repository `.env` must remain uncommitted. This repository ignores `.env` and `.env.*` by default; copy the same rules into repositories that do not already ignore them. Use `.env.example` for placeholder documentation only—never put a real token in it.
89+
The symlinks install the same skill for Codex and Claude Code without copying it.
4490

45-
The service URL resolves separately:
91+
### 3. Attach a file
4692

47-
| Priority | Location | Scope |
48-
| ---: | --- | --- |
49-
| 1 | `GITHUB_ATTACHMENTS_URL` in the process environment | Current process override |
50-
| 2 | `${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/url` | User-level deployment |
93+
Ask the agent to attach a file to a pull request, or use the CLI directly:
5194

52-
The URL is required; there is no shared-service fallback. Repository `.env` cannot redirect a user-level token to another service. User profile files and repository `.env` are parsed as data, never sourced as shell code.
95+
```bash
96+
github-attach screenshot.png --alt "Settings after the change"
97+
```
5398

54-
The CLI prints only Markdown by default:
99+
The command prints Markdown:
55100

56101
```markdown
57102
![Settings after the change](https://your-worker.your-subdomain.workers.dev/a/.../screenshot.png)
58103
```
59104

60105
Pass `--json` to receive the complete upload response. Diagnostics and errors go to standard error.
61106

62-
The raw API is equally small:
107+
## Deployment options
108+
109+
Each deployment has one Worker, one upload token, and one statically bound R2 bucket. Upload requests cannot select or override the bucket.
110+
111+
To reuse an existing private bucket, omit `--create-bucket` and choose a unique prefix:
63112

64113
```bash
65-
curl --fail-with-body \
66-
-H "Authorization: Bearer $GITHUB_ATTACHMENTS_TOKEN" \
67-
-H "X-Filename: screenshot.png" \
68-
-H "Content-Type: image/png" \
69-
--data-binary @screenshot.png \
70-
"$GITHUB_ATTACHMENTS_URL/v1/attachments"
114+
npm run setup -- \
115+
--worker github-pr-attachments-yourname \
116+
--bucket your-existing-private-bucket \
117+
--prefix github-pr-attachments-yourname/objects/
71118
```
72119

73-
See [openapi.yaml](openapi.yaml) for the complete contract.
120+
The default prefix is `<worker-name>/objects/`; the default retention is 180 days. Use `--prefix` and `--retention-days` to change them. Distinct prefixes allow multiple deployments to share one bucket safely.
74121

75-
## Architecture
122+
Setup generates an ignored `wrangler.user.jsonc` and writes the runtime profile to:
76123

77124
```text
78-
Agent -> authenticated Worker -> atomic quota reservation -> private R2
79-
GitHub -> opaque public URL -> Worker security headers -> private R2
125+
${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/url
126+
${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/token
80127
```
81128

82-
- The Worker validates requests, authenticates mutations, generates Markdown, and streams bodies.
83-
- R2 remains private; there is no `r2.dev` or public bucket endpoint.
84-
- One SQLite-backed Durable Object is the coordination atom for this single service quota.
85-
- Durable Object alarms delete expired objects and abandoned reservations.
86-
- An R2 lifecycle rule provides expiration defense in depth.
129+
The upload token can use this service but cannot administer Cloudflare.
87130

88-
Only PNG, JPEG, GIF, and WebP responses are rendered inline. Every other media type is sent with an attachment disposition, `nosniff`, and a restrictive content security policy.
131+
## Configuration reference
89132

90-
## Limits
133+
### Upload token
91134

92-
| Limit | Default |
93-
| --- | ---: |
94-
| Stored data | 8 GB |
95-
| Objects | 50,000 |
96-
| Raster image size | 10 MB |
97-
| Other file size | 25 MB |
98-
| Upload attempts per day | 1,000 |
99-
| Upload attempts per month | 100,000 |
100-
| Retention | 180 days |
135+
The CLI and agent skill use the first nonempty token in this order:
101136

102-
Reservations are atomic, including under concurrent uploads. Failed attempts still count toward daily and monthly operation limits, which keeps abuse accounting conservative.
137+
| Priority | Location | Scope |
138+
| ---: | --- | --- |
139+
| 1 | `GITHUB_ATTACHMENTS_TOKEN` in the repository-root `.env` | Repository override |
140+
| 2 | `GITHUB_ATTACHMENTS_TOKEN` in the process environment | Process override |
141+
| 3 | `${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/token` | User default |
103142

104-
## Local development
143+
For a repository-specific token:
105144

106-
```bash
107-
cp .dev.vars.example .dev.vars
108-
npm install
109-
npm run types
110-
npm run check
111-
npm run dev
145+
```dotenv
146+
GITHUB_ATTACHMENTS_TOKEN=repository-specific-token
112147
```
113148

114-
`npm run check` runs Biome, generated binding drift checks, strict TypeScript checks, Workers-runtime integration tests, and the CLI integration test.
149+
Keep the repository `.env` uncommitted. User profile files and `.env` are parsed as data rather than sourced as shell code.
115150

116-
## Deploy your own service
151+
### Service URL
117152

118-
Each deployment has one Worker, one upload token, and one statically bound R2 bucket. Upload requests cannot choose or override the bucket.
153+
The service URL is required and resolves separately:
119154

120-
Install dependencies, then create a least-privilege Cloudflare API token with these account permissions:
155+
| Priority | Location | Scope |
156+
| ---: | --- | --- |
157+
| 1 | `GITHUB_ATTACHMENTS_URL` in the process environment | Process override |
158+
| 2 | `${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/url` | User deployment |
121159

122-
- Workers Scripts: Edit
123-
- Workers R2 Storage: Edit
124-
- Account Settings: Read
160+
Repository `.env` cannot redirect a user-level token to another service, and there is no shared-service fallback.
125161

126-
Limit it to the target account. A short expiration and client-IP restriction further reduce its blast radius when appropriate. Save it outside Git:
162+
### HTTP API
163+
164+
The raw API accepts the file body directly:
127165

128166
```bash
129-
deploy_config="${XDG_CONFIG_HOME:-$HOME/.config}/github-pr-attachments"
130-
mkdir -p "$deploy_config"
131-
chmod 700 "$deploy_config"
132-
$EDITOR "$deploy_config/deploy.env"
133-
chmod 600 "$deploy_config/deploy.env"
167+
curl --fail-with-body \
168+
-H "Authorization: Bearer $GITHUB_ATTACHMENTS_TOKEN" \
169+
-H "X-Filename: screenshot.png" \
170+
-H "Content-Type: image/png" \
171+
--data-binary @screenshot.png \
172+
"$GITHUB_ATTACHMENTS_URL/v1/attachments"
134173
```
135174

136-
`deploy.env` contains:
137-
138-
```dotenv
139-
CLOUDFLARE_ACCOUNT_ID=your-account-id
140-
CLOUDFLARE_API_TOKEN=your-deployment-token
141-
```
175+
See [openapi.yaml](openapi.yaml) for the complete contract.
142176

143-
Create a new private bucket and deploy:
177+
## Technical overview
144178

145-
```bash
146-
npm install
147-
npm run setup -- \
148-
--worker github-pr-attachments-yourname \
149-
--bucket github-pr-attachments-yourname \
150-
--create-bucket
179+
```text
180+
Agent -> authenticated Worker -> atomic quota reservation -> private R2
181+
GitHub -> opaque public URL -> Worker security headers -> private R2
151182
```
152183

153-
To reuse an existing private bucket, omit `--create-bucket`:
154-
155-
```bash
156-
npm run setup -- \
157-
--worker github-pr-attachments-yourname \
158-
--bucket your-existing-private-bucket \
159-
--prefix github-pr-attachments-yourname/objects/
160-
```
184+
- The Worker authenticates mutations, validates requests, streams file bodies, and generates Markdown.
185+
- R2 remains private; downloads pass through the Worker.
186+
- A SQLite-backed Durable Object coordinates the service-wide quota.
187+
- Durable Object alarms clean up expired objects and abandoned reservations.
188+
- A prefix-scoped R2 lifecycle rule provides expiration defense in depth.
161189

162-
The setup command:
190+
PNG, JPEG, GIF, and WebP responses can render inline. Other media types are served as downloads with `nosniff` and a restrictive content security policy.
163191

164-
1. Verifies or creates the named bucket.
165-
2. Adds a prefix-scoped R2 lifecycle rule.
166-
3. Generates `wrangler.user.jsonc` with the selected Worker, bucket, prefix, and retention.
167-
4. Generates a 256-bit upload token and deploys it as a Worker secret.
168-
5. waits for `/healthz`, then writes the user-level `url` and `token` profile.
192+
## Default limits
169193

170-
The default prefix is `<worker-name>/objects/`; the default retention is 180 days. Use `--prefix` and `--retention-days` to change them. Prefixes let multiple deployments safely share one bucket, provided every deployment uses a distinct prefix. `wrangler.user.jsonc`, `.env*`, `.dev.vars`, and `.github-attachments/` are ignored by Git.
194+
| Limit | Default |
195+
| --- | ---: |
196+
| Stored data | 8 GB |
197+
| Objects | 50,000 |
198+
| Raster image size | 10 MB |
199+
| Other file size | 25 MB |
200+
| Upload attempts per day | 1,000 |
201+
| Upload attempts per month | 100,000 |
202+
| Retention | 180 days |
171203

172-
The Cloudflare API token is needed only for provisioning and later redeployment. Remove or revoke it after setup if the machine should retain upload access but not deployment access. The user-level `token` is deliberately separate: it can upload through this service but cannot administer Cloudflare.
204+
Reservations are atomic under concurrent uploads. Failed attempts still count toward daily and monthly operation limits.
173205

174206
## Operations
175207

@@ -190,17 +222,31 @@ curl --fail-with-body -X DELETE \
190222
"$GITHUB_ATTACHMENTS_URL/v1/attachments/$ATTACHMENT_ID"
191223
```
192224

193-
Cloudflare budget alerts should be enabled as defense in depth. They do not stop spending; the application limits, private R2 bucket, and Workers Free request ceiling are the cost boundaries. A dedicated Cloudflare account prevents unrelated usage from consuming the same allowances.
225+
Cloudflare budget alerts provide additional visibility but do not stop spending. The application limits, private bucket, and platform request limits are the primary cost boundaries.
226+
227+
## Local development
228+
229+
```bash
230+
cp .dev.vars.example .dev.vars
231+
npm install
232+
npm run types
233+
npm run check
234+
npm run dev
235+
```
236+
237+
`npm run check` runs formatting and lint checks, generated binding drift checks, strict TypeScript checks, Workers-runtime integration tests, and CLI/setup tests.
194238

195239
## Security model
196240

197-
- A 256-bit bearer token protects upload, delete, and quota routes.
241+
- A random 256-bit bearer token protects upload, delete, and quota routes.
198242
- Secret comparison uses Cloudflare's constant-time Web Crypto operation.
199243
- Public IDs are cryptographically random UUIDs and cannot be listed through the service.
200244
- User filenames never become R2 keys and cannot set response headers.
201245
- Public URLs are capability URLs, not private-repository authorization.
202246
- The service does not scan downloads for malware.
203247

204-
## Design
248+
## Design and license
205249

206250
See [DESIGN.md](DESIGN.md) for the original proposal and design rationale.
251+
252+
This project is available under the [MIT License](LICENSE).

0 commit comments

Comments
 (0)