Skip to content

Commit ff9a312

Browse files
authored
v1.0.0 release prep: docs rewrite, CI, dependabot, --version, SECURITY.md
Two prior commits' worth of v1.0.0 readiness work, squashed: - README rewrite to production-grade layout (hero banner, badges, restructured Installation/Usage/Authentication/CI-CD) - documentation/architecture.md refreshed for current packages, multi-browser support, S3 corrections; mermaid data-flow diagram - LICENSE: extend copyright to 2025-2026 - .github/workflows/test.yml: PR-time CI (vet/test/build) - .github/dependabot.yml: weekly gomod + github-actions updates - main.go + .goreleaser.yml: --version flag wired through ldflags - SECURITY.md: private vulnerability reporting policy - README note: Chrome 127+ on Windows not yet supported (kooky upstream, tracking #4) - Bumped actions/checkout and actions/setup-go to v6 (Node 24)
1 parent 8788e61 commit ff9a312

9 files changed

Lines changed: 345 additions & 214 deletions

File tree

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: gomod
4+
directory: "/"
5+
schedule:
6+
interval: weekly
7+
- package-ecosystem: github-actions
8+
directory: "/"
9+
schedule:
10+
interval: weekly

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@v4
16+
uses: actions/checkout@v6
1717
with:
1818
fetch-depth: 0
1919

2020
- name: Set up Go
21-
uses: actions/setup-go@v5
21+
uses: actions/setup-go@v6
2222
with:
2323
go-version: stable
2424

.github/workflows/test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v6
13+
- uses: actions/setup-go@v6
14+
with:
15+
go-version: stable
16+
- run: go vet ./...
17+
- run: go test -race -cover ./...
18+
- run: go build ./...

.goreleaser.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ builds:
66
env:
77
- CGO_ENABLED=0
88
ldflags:
9-
- -s -w
9+
- -s -w -X main.version={{.Version}}
1010
goos:
1111
- darwin
1212
- linux

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 drogers0
3+
Copyright (c) 2025-2026 drogers0
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 132 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,201 +1,199 @@
1-
# gh-image
1+
<p align="center">
2+
<img src="https://github.com/user-attachments/assets/92463e67-b897-4212-91b4-a4f9b80ec4d4" alt="gh-image banner" width="640">
3+
</p>
24

3-
![banner](https://github.com/user-attachments/assets/92463e67-b897-4212-91b4-a4f9b80ec4d4)
5+
<p align="center">
6+
<em>Drop images into GitHub issues, PRs, and READMEs, straight from the command line.</em>
7+
</p>
48

5-
A `gh` CLI extension that uploads images to GitHub from the command line.
9+
<p align="center">
10+
<a href="https://github.com/drogers0/gh-image/releases/latest"><img src="https://img.shields.io/github/v/release/drogers0/gh-image?color=blue" alt="Latest release"></a>
11+
<a href="https://github.com/drogers0/gh-image/stargazers"><img src="https://img.shields.io/github/stars/drogers0/gh-image?style=flat&color=yellow" alt="GitHub stars"></a>
12+
<a href="https://github.com/drogers0/gh-image/releases"><img src="https://img.shields.io/github/downloads/drogers0/gh-image/total?color=green" alt="Total downloads"></a>
13+
<a href="LICENSE"><img src="https://img.shields.io/github/license/drogers0/gh-image?color=lightgrey" alt="License: MIT"></a>
14+
<a href="https://goreportcard.com/report/github.com/drogers0/gh-image"><img src="https://goreportcard.com/badge/github.com/drogers0/gh-image" alt="Go Report Card"></a>
15+
</p>
616

7-
GitHub has no public API for image uploads — the web UI uses an internal endpoint that produces URLs scoped to a repository's visibility. This tool replicates that flow, so images on private repos stay private.
17+
---
818

9-
## Usage
19+
GitHub has no public API for image uploads. The web UI uses an internal endpoint that produces `user-attachments` URLs whose visibility is scoped to the repository they were uploaded to. `gh-image` replicates that flow as a `gh` CLI extension, so you can drop a screenshot into a bug report, README, or Slack thread without leaving the terminal — and images on private repos stay private.
1020

11-
```bash
12-
# Upload an image and get the markdown (infers repo from current git workspace)
13-
gh image screenshot.png
21+
```console
22+
$ gh image screenshot.png
23+
![screenshot.png](https://github.com/user-attachments/assets/88f4599a-…-bc24)
24+
```
1425

15-
# Upload multiple images
16-
gh image img1.png img2.png
26+
## Installation
1727

18-
# Explicit repo (when not in a git workspace or targeting a different repo)
19-
gh image screenshot.png --repo owner/repo
28+
```bash
29+
gh extension install drogers0/gh-image
2030
```
2131

22-
Output:
23-
```
24-
![screenshot](https://github.com/user-attachments/assets/<uuid>)
32+
That's it. The [`gh` CLI](https://cli.github.com) auto-detects your platform and downloads the prebuilt binary. Pre-built releases ship for **macOS** (arm64, amd64), **Linux** (amd64, arm64), and **Windows** (amd64).
33+
34+
<details>
35+
<summary>Build from source</summary>
36+
37+
```bash
38+
git clone https://github.com/drogers0/gh-image
39+
cd gh-image
40+
go build -o gh-image
41+
gh extension install .
2542
```
2643

27-
### Example: Upload an image and attach it to a GitHub issue
44+
Requires Go 1.26+.
45+
46+
</details>
47+
48+
## Usage
2849

2950
```bash
30-
# Upload the image
31-
gh image screenshot.png --repo owner/repo
32-
# Output: ![screenshot.png](https://github.com/user-attachments/assets/88f4599a-...)
51+
# Upload an image (infers repo from the current git workspace)
52+
gh image screenshot.png
3353

34-
# Use the output in a new issue
35-
gh issue create --repo owner/repo \
36-
--title "Bug report" \
37-
--body "Here's what I see:
54+
# Upload multiple images at once
55+
gh image hero.png diagram.png chart.png
3856

39-
![screenshot.png](https://github.com/user-attachments/assets/88f4599a-...)
40-
"
57+
# Target a specific repository
58+
gh image screenshot.png --repo owner/repo
4159
```
4260

43-
## Install
61+
Each successful upload prints a ready-to-paste markdown reference on its own line:
4462

45-
```bash
46-
gh extension install drogers0/gh-image
63+
```
64+
![hero.png](https://github.com/user-attachments/assets/…)
65+
![diagram.png](https://github.com/user-attachments/assets/…)
4766
```
4867

49-
## How It Works
68+
If any upload fails, the error is printed to stderr and the process exits non-zero — other images in the batch still upload.
5069

51-
1. Reads your GitHub session cookie from your browser's local cookie store (encrypted cookie decryption handled automatically)
52-
2. Uploads the image through GitHub's internal asset upload flow (the same one the web UI uses)
53-
3. Prints a markdown image reference to stdout
70+
### Pipe directly into an issue, PR, or comment
5471

55-
The upload produces `https://github.com/user-attachments/assets/<uuid>` URLs — the same format as drag-and-drop uploads in the browser. Images inherit the repository's visibility: private repo images require authentication to view.
72+
From inside the repo's working directory, both `gh image` and `gh issue create` infer the target repository automatically:
5673

57-
See [documentation/github-image-upload-flow.md](documentation/github-image-upload-flow.md) for the full reverse-engineered upload protocol.
74+
```bash
75+
gh issue create \
76+
--title "Login button stuck in loading state" \
77+
--body "Repro on staging:
78+
79+
$(gh image bug.png)
80+
81+
Happens consistently after the third click."
82+
```
5883

5984
## Authentication
6085

61-
No tokens or OAuth setup required. The tool reads your `user_session` cookie directly from your browser's cookie database on disk. On macOS, a Keychain prompt may appear on first use to authorize access to the browser's encryption key.
86+
`gh-image` authenticates with your existing GitHub session — **no tokens to provision, no OAuth scopes to configure** for everyday local use. The tool reads the `user_session` cookie from your browser's encrypted cookie store.
6287

63-
Supported browsers:
64-
- Chrome
65-
- Brave
66-
- Chromium
67-
- Edge
68-
- Firefox
69-
- Opera
70-
- Safari
88+
**Supported browsers:** Chrome · Brave · Chromium · Edge · Firefox · Opera · Safari
7189

72-
Supported platforms:
73-
- macOS
74-
- Linux
75-
- Windows
90+
**Supported platforms:** macOS · Linux · Windows
7691

77-
## Session Token
92+
On macOS, a Keychain prompt may appear on first use to authorize access to your browser's cookie encryption key. Click **Always Allow** to skip future prompts.
7893

79-
The tool supports providing your own session token instead of reading from the browser. This enables use in environments where browser cookies are unavailable (such as CI).
94+
> [!NOTE]
95+
> **Windows + Chrome 127+:** Some versions of Chrome on Windows are not yet supported by the underlying cookie library. Use another browser or [investigate potential workarounds](https://github.com/drogers0/gh-image/issues/4).
8096
81-
> **Warning:** `user_session` cookies grant full account access — they are not
82-
> scoped like personal access tokens. Treat them with the same care as a password.
83-
> Rotate promptly if leaked.
97+
### Session token override
8498

85-
**Token resolution order (highest priority first):**
99+
For CI, headless environments, or shared machines, you can supply the session token explicitly. Resolution order (first match wins):
86100

87-
1. `--token <value>` flag
88-
2. `GH_SESSION_TOKEN` environment variable
89-
3. Browser cookie extraction (default)
101+
| Priority | Source | When to use |
102+
|---|---|---|
103+
| 1 | `--token <value>` flag | One-off invocations |
104+
| 2 | `GH_SESSION_TOKEN` env var | CI/CD, shared machines |
105+
| 3 | Browser cookie store | Local interactive use (default) |
90106

91107
```bash
92-
# Use a token via flag
108+
# Flag (visible in process listings like `ps aux` — avoid on shared machines)
93109
gh image --token "$MY_TOKEN" screenshot.png --repo owner/repo
94110

95-
# Use a token via environment variable (preferred on shared machines)
111+
# Environment variable (preferred — not visible to `ps aux`)
96112
GH_SESSION_TOKEN="$MY_TOKEN" gh image screenshot.png --repo owner/repo
97113
```
98114

99-
> **Security note:** `--token` values are visible in process listings (`ps aux`).
100-
> Use the `GH_SESSION_TOKEN` environment variable on shared machines.
115+
> [!WARNING]
116+
> `user_session` cookies grant **full account access** — they are not scoped like personal access tokens. Treat them with the same care as a password. If leaked, **[sign out of GitHub](https://github.com/logout)** on the machine that holds the session; if you are not on that machine, revoke it through [Settings → Sessions](https://github.com/settings/sessions), or [change your password](https://github.com/settings/security) (which kills every session in one action).
101117
102118

103-
## Token Utilities
119+
## CI / CD
104120

105-
### `extract-token`
121+
`gh-image` runs unattended in GitHub Actions when given a session token via `GH_SESSION_TOKEN`.
106122

107-
Extracts your session token from the browser and prints it to stdout. Useful for storing your token as a CI secret.
123+
> [!CAUTION]
124+
> **Use a dedicated bot account for CI/CD on shared repos.** GitHub hides secret values in the UI and masks log emissions, but a determined collaborator with write access can craft a workflow that exfiltrates the value through channels masking doesn't cover. Storing your *personal* `user_session` means such a leak compromises your account; a bot account scopes the blast radius to that bot. Decide whose token to extract in step 1 below accordingly.
108125
109-
```bash
110-
# Extract and print token
111-
gh image extract-token
126+
**Setup**
112127

113-
# Store in a variable
114-
TOKEN=$(gh image extract-token)
115-
```
128+
1. Run `gh image extract-token` locally to capture the token (token → stdout, status → stderr), then run `gh image check-token --token <token>` to confirm it authenticates as the intended user (username → stdout on success, exit code `0` = valid).
129+
2. Create a GitHub environment (Settings → Environments → New environment), e.g. `gh-image`, and restrict deployment branches to a trusted set (e.g. `main` only).
130+
3. Add the token as an **environment secret** named `GH_SESSION_TOKEN` on that environment.
116131

117-
### `check-token`
132+
```yaml
133+
jobs:
134+
upload:
135+
runs-on: ubuntu-latest
136+
environment: gh-image # binds this job to the scoped environment
137+
steps:
138+
- name: Upload screenshots
139+
env:
140+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for gh CLI auth
141+
GH_SESSION_TOKEN: ${{ secrets.GH_SESSION_TOKEN }} # for the upload itself
142+
run: |
143+
gh extension install drogers0/gh-image
144+
gh image check-token # optional: fail fast if the session expired
145+
gh image screenshot.png --repo ${{ github.repository }}
146+
```
118147
119-
Verifies that a session token is valid and, on a best-effort basis, prints the authenticated GitHub username to stdout. Exit code 0 means valid, exit code 1 means invalid or an error occurred. Username output may be empty even when the token is valid (for example, if the `<meta name="user-login">` tag is missing or cannot be parsed), so rely on the exit code for validity checks.
148+
> [!NOTE]
149+
> `user_session` cookies expire when GitHub invalidates the session. A scheduled `check-token` job is the cleanest way to detect expiry before it breaks a real run.
120150

121-
```bash
122-
# Check token from flag
123-
gh image check-token --token "$MY_TOKEN"
151+
## How it works
124152

125-
# Check token from environment variable
126-
GH_SESSION_TOKEN="$MY_TOKEN" gh image check-token
153+
1. Resolves a `user_session` cookie from the configured source (flag → env → browser).
154+
2. Fetches the target repository's page to obtain an `uploadToken` from the embedded JS payload.
155+
3. Requests an S3 upload policy from `/upload/policies/assets`.
156+
4. Uploads the file directly to S3 using the presigned form fields.
157+
5. Calls back to GitHub to finalize the asset.
158+
6. Prints `![name](url)` to stdout.
127159

128-
# Check token from browser (default)
129-
gh image check-token
160+
The final URL is the standard `https://github.com/user-attachments/assets/<uuid>` format — visibility inherits from the target repository, so a private-repo upload requires authentication to view.
130161

131-
# Capture username in a script
132-
USERNAME=$(gh image check-token --token "$MY_TOKEN")
133-
```
162+
For the full architecture, see **[documentation/architecture.md](documentation/architecture.md)**. For the reverse-engineered upload protocol, see **[documentation/github-image-upload-flow.md](documentation/github-image-upload-flow.md)**.
134163

135-
## CI/CD Usage
164+
## Requirements
136165

137-
> **Note:** `user_session` cookies are real user login sessions, not scoped automation credentials. They expire when GitHub invalidates the session. Use a pre-push hook or periodic job to validate the token (see `check-token`) and rotate it when needed.
166+
- A supported browser with an active GitHub session — or a `GH_SESSION_TOKEN` for CI.
167+
- Write access to the target repository (uploads require it).
168+
- A target repository — pass `--repo owner/repo`, or run from a git workspace whose `origin` remote is on GitHub.
169+
- The `gh` CLI must be installed and authenticated (used for repository ID lookup).
138170

139-
### Setup
171+
## Limitations
140172

141-
1. Extract your token locally:
142-
```bash
143-
gh image extract-token
144-
```
145-
2. Add the output as a repository secret named `GH_SESSION_TOKEN` in your GitHub repository settings.
173+
- Uses an **undocumented** internal GitHub API that may change without notice.
174+
- `uploadToken` is only issued to users with write access on the target repository.
175+
- Session cookies are not scoped credentials; they expire when GitHub invalidates the session.
146176

147-
### Example GitHub Actions workflow
177+
## Contributing
148178

149-
```yaml
150-
- name: Upload screenshots
151-
env:
152-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Required for gh CLI auth
153-
GH_SESSION_TOKEN: ${{ secrets.GH_SESSION_TOKEN }} # GitHub session token
154-
run: |
155-
gh extension install drogers0/gh-image
156-
gh image --repo owner/repo screenshot.png
157-
```
179+
Issues and pull requests are welcome. For bug reports, please include:
158180

159-
> **Note:** `gh` CLI must be authenticated (via `GH_TOKEN` or equivalent) for
160-
> extension installation and repository ID lookup. `GH_SESSION_TOKEN` is the
161-
> session token used for the image upload itself.
181+
- Your OS and browser
182+
- The exact `gh image` invocation
183+
- The error output (with any session token values redacted)
162184

163-
### Validate token before upload
185+
Before opening a PR, run `go test ./...` and `go vet ./...`.
164186

165-
```yaml
166-
- name: Validate session token
167-
env:
168-
GH_SESSION_TOKEN: ${{ secrets.GH_SESSION_TOKEN }}
169-
run: gh image check-token
170-
171-
- name: Upload screenshots
172-
env:
173-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
174-
GH_SESSION_TOKEN: ${{ secrets.GH_SESSION_TOKEN }}
175-
run: |
176-
gh extension install drogers0/gh-image
177-
gh image --repo owner/repo screenshot.png
178-
```
187+
## Support
179188

180-
### Cache extension for faster CI runs
189+
If `gh-image` saves you a few drag-and-drops, a ⭐ helps others find it:
181190

182-
```yaml
183-
- name: Cache gh extensions
184-
uses: actions/cache@v4
185-
with:
186-
path: ~/.local/share/gh/extensions
187-
key: gh-extensions-${{ runner.os }}
191+
```bash
192+
gh api --method PUT user/starred/drogers0/gh-image
188193
```
189194

190-
## Requirements
195+
(or just click the star at the [top of this page](https://github.com/drogers0/gh-image))
191196

192-
- A supported browser with an active GitHub session (for local use)
193-
- Write access to the target repository
194-
- For CI: a valid `GH_SESSION_TOKEN` secret (see [CI/CD Usage](#cicd-usage))
195-
196-
## Limitations
197+
## License
197198

198-
- This tool uses an undocumented GitHub internal API that could change without notice.
199-
- The `uploadToken` required for uploads is only available to users with write access to the target repository.
200-
- Either `--repo` must be provided or the tool must be run from within a git workspace with a GitHub remote.
201-
- Session tokens are not scoped credentials — they expire with the browser session and should be rotated periodically.
199+
[MIT](LICENSE) © 2025-2026 drogers0

0 commit comments

Comments
 (0)