Skip to content

🐛 Screenshots as compressed comments + GHA processing#4342

Merged
clubanderson merged 1 commit intomainfrom
fix/screenshot-gist-upload
Apr 2, 2026
Merged

🐛 Screenshots as compressed comments + GHA processing#4342
clubanderson merged 1 commit intomainfrom
fix/screenshot-gist-upload

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Fixes the 422 "body is too long" error from #4220. Base64 screenshots easily exceed GitHub's 65K issue body limit.

New approach

  1. Frontend compresses screenshots (JPEG, max 1024px, quality 60%) → ~30KB per image
  2. Backend adds screenshots as separate issue comments — one per screenshot, each with a <!-- screenshot-base64:N --> marker. Issue body stays clean and small.
  3. GHA workflow (process-screenshots.yml) triggers on issue_comment.created, decodes base64, commits image to .github/screenshots/, replaces the comment with ![Screenshot](url)

Token permissions simplified

Since screenshots go via issue comments (not Contents API), the token no longer needs Contents: Read and write. Updated github-token.ts constants — only Issues: Read and write is needed for fine-grained PATs.

Files changed

  • pkg/api/handlers/feedback.go — screenshots as comments instead of in body
  • .github/workflows/process-screenshots.yml — triggers on issue_comment instead of issues.opened
  • web/src/lib/imageCompression.ts — new: client-side JPEG compression
  • web/src/components/feedback/*.tsx — use compression, updated messages
  • web/src/lib/constants/github-token.ts — removed Contents permission requirement

Test plan

  • Submit feedback with screenshot → issue created, screenshot added as comment with <details> block
  • GHA workflow triggers on the comment → replaces with rendered image
  • Large screenshot (>200KB) compressed to fit in 65K comment limit
  • Multiple screenshots → each gets its own comment
  • Classic PAT with repo scope only — works without Contents permission

The previous base64-in-issue-body approach hit GitHub's 65K character
body limit. Screenshots are now:

1. Compressed on the frontend (JPEG, max 1024px, quality 60%) to ~30KB
2. Added as separate issue comments (one per screenshot) — not in the body
3. Processed by GHA workflow (process-screenshots.yml) which triggers on
   issue_comment, decodes base64, commits image, replaces comment with
   rendered ![Screenshot](url)

This avoids both the body size limit and the push access requirement.

Also updated:
- Token permissions: Contents scope no longer needed (only Issues R/W)
- github-token.ts constants updated to reflect reduced requirements
- Frontend messages reflect the new async processing flow

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Copilot AI review requested due to automatic review settings April 2, 2026 22:04
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 2, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mikespreitzer for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 2, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit f4c7d58
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cee7e7ba794d000896f66a
😎 Deploy Preview https://deploy-preview-4342.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@clubanderson clubanderson merged commit 9a33bd1 into main Apr 2, 2026
15 of 16 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow bot deleted the fix/screenshot-gist-upload branch April 2, 2026 22:04
@kubestellar-prow kubestellar-prow bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Apr 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Addresses GitHub’s 65K issue body limit by moving screenshot payloads out of the issue body and into compressed, per-screenshot issue comments that a GitHub Actions workflow later converts into committed images.

Changes:

  • Add client-side screenshot compression (JPEG + resize) before submitting feedback.
  • Backend now creates issues with a small body and posts each screenshot as a separate issue comment with a marker for automation.
  • Update GitHub Actions workflow to trigger on issue_comment.created and replace the base64 comment with a rendered image after committing it to the repo.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
web/src/lib/imageCompression.ts Adds client-side compression to keep base64 under GitHub limits.
web/src/lib/constants/github-token.ts Updates fine-grained PAT permission guidance (removes Contents scope requirement for users).
web/src/components/feedback/FeedbackModal.tsx Uses compressScreenshot() prior to submitting screenshots.
web/src/components/feedback/FeatureRequestModal.tsx Uses compressScreenshot() prior to submitting screenshots.
pkg/api/handlers/feedback.go Moves screenshot payloads from issue body into per-screenshot issue comments.
.github/workflows/process-screenshots.yml Processes screenshot marker comments, commits decoded images, and updates the comment to a markdown image link.
Comments suppressed due to low confidence (1)

web/src/components/feedback/FeedbackModal.tsx:182

  • The inline comments still mention fitting within the issue body limit and that screenshots are “uploaded server-side and embedded as images”, but the new design is “base64 in issue comments processed by GHA”. Please update these comments to reflect the new comment-based pipeline to avoid misleading future edits.
      // Compress screenshots to fit within GitHub's 65K issue body limit.
      // Images are embedded as base64 and processed into rendered images
      // by a GitHub Actions workflow after the issue is created.
      const screenshotDataURIs: string[] = []
      for (const s of screenshots) {
        const compressed = await compressScreenshot(s.preview)
        if (compressed) screenshotDataURIs.push(compressed)
      }

      // Submit via backend API — creates GitHub issue directly using the
      // server-side token. No GitHub login required from the user.
      // Screenshots are uploaded server-side and embedded as images.
      const hasScreenshots = screenshotDataURIs.length > 0

Comment on lines +2 to +9
* Compress an image data URI to fit within GitHub's issue body limit.
*
* Screenshots are embedded as base64 in issue bodies and processed into
* rendered images by a GitHub Actions workflow. GitHub limits issue bodies
* to 65,536 characters, so we compress images to JPEG and resize if needed.
*
* Target: ~30KB per screenshot (~40K base64 chars), leaving room for
* the issue template and multiple screenshots.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module docstring still describes screenshots being embedded in issue bodies, but this PR’s approach embeds base64 in issue comments (and GitHub has similar ~65K limits on both bodies and comments). Updating the top-level comment to match the new flow will prevent confusion for future maintainers.

Suggested change
* Compress an image data URI to fit within GitHub's issue body limit.
*
* Screenshots are embedded as base64 in issue bodies and processed into
* rendered images by a GitHub Actions workflow. GitHub limits issue bodies
* to 65,536 characters, so we compress images to JPEG and resize if needed.
*
* Target: ~30KB per screenshot (~40K base64 chars), leaving room for
* the issue template and multiple screenshots.
* Compress an image data URI to fit within GitHub's issue comment limit.
*
* Screenshots are embedded as base64 in issue comments and processed into
* rendered images by a GitHub Actions workflow. GitHub limits issue comments
* to about 65,536 characters, so we compress images to JPEG and resize if needed.
*
* Target: ~30KB per screenshot (~40K base64 chars), leaving room for
* other comment content and multiple screenshots.

Copilot uses AI. Check for mistakes.
Comment on lines +174 to 178
for (const s of screenshots) {
const compressed = await compressScreenshot(s.preview)
if (compressed) screenshotDataURIs.push(compressed)
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshots that fail client-side compression are silently dropped (compressScreenshot returns null and the screenshot is not included), but the UI still shows the screenshot thumbnails and no warning is displayed. Consider tracking a client-side failed count and surfacing a message (or blocking submit) when any screenshot cannot be compressed so users know it won’t be attached.

Suggested change
for (const s of screenshots) {
const compressed = await compressScreenshot(s.preview)
if (compressed) screenshotDataURIs.push(compressed)
}
let failedCompressionCount = 0
for (const s of screenshots) {
const compressed = await compressScreenshot(s.preview)
if (compressed) {
screenshotDataURIs.push(compressed)
} else {
failedCompressionCount += 1
}
}
if (failedCompressionCount > 0) {
const message = failedCompressionCount === 1
? '1 screenshot could not be compressed and will not be attached. Please remove it or try again.'
: `${failedCompressionCount} screenshots could not be compressed and will not be attached. Please remove them or try again.`
emitScreenshotUploadFailed(message, failedCompressionCount)
setSubmitError(message)
showToast(message, 'error')
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +345 to 349
for (const s of screenshots) {
const compressed = await compressScreenshot(s.preview)
if (compressed) screenshotDataURIs.push(compressed)
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshots that fail client-side compression are currently omitted without any user-visible warning (compressScreenshot can return null, and those screenshots are just not sent). Consider counting compression failures and showing a warning (or preventing submit) so users don’t assume all selected screenshots were attached.

Suggested change
for (const s of screenshots) {
const compressed = await compressScreenshot(s.preview)
if (compressed) screenshotDataURIs.push(compressed)
}
let screenshotCompressionFailures = 0
for (const s of screenshots) {
const compressed = await compressScreenshot(s.preview)
if (compressed) {
screenshotDataURIs.push(compressed)
} else {
screenshotCompressionFailures += 1
}
}
if (screenshotCompressionFailures > 0) {
setError(
`Failed to process ${screenshotCompressionFailures} screenshot${screenshotCompressionFailures === 1 ? '' : 's'}. Please remove or re-add the affected screenshot${screenshotCompressionFailures === 1 ? '' : 's'} and try again.`
)
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +1568 to 1585
// Validate screenshots upfront so we can report accurate counts.
// Screenshots are NOT embedded in the issue body (GitHub limits bodies to
// 65,536 chars and base64 screenshots easily exceed that). Instead, they
// are added as separate comments after issue creation. A GitHub Actions
// workflow (process-screenshots.yml) then decodes the base64, commits
// images to the repo, and replaces the comment with a rendered image.
var validScreenshots []string
var ssResult screenshotUploadResult
if len(screenshots) > 0 {
var blocks []string
for i, dataURI := range screenshots {
// Validate the data URI format
parts := strings.SplitN(dataURI, ",", 2)
if len(parts) != 2 {
ssResult.Failed++
log.Printf("[Feedback] Screenshot %d: invalid data URI format", i+1)
continue
}
ssResult.Uploaded++
// Wrap in a collapsible <details> block with a machine-readable marker
// that the GHA workflow can find and process.
blocks = append(blocks, fmt.Sprintf(
"<!-- screenshot-base64:%d -->\n<details>\n<summary>Screenshot %d (processing...)</summary>\n\n```\n%s\n```\n\n</details>",
i+1, i+1, dataURI))
}
if len(blocks) > 0 {
screenshotMarkdown = "\n\n## Screenshots\n\n" + strings.Join(blocks, "\n\n")
for i, dataURI := range screenshots {
parts := strings.SplitN(dataURI, ",", 2)
if len(parts) != 2 {
ssResult.Failed++
log.Printf("[Feedback] Screenshot %d: invalid data URI format", i+1)
continue
}
validScreenshots = append(validScreenshots, dataURI)
ssResult.Uploaded++
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

screenshotUploadResult is now incremented based only on data-URI parsing, not on whether the screenshot comment was actually created successfully. Since addIssueComment only logs failures and returns void, the API can report screenshots_uploaded even when GitHub rejects the comment (permissions/size/rate-limit). Consider making addIssueComment return an error (or status code) and update ssResult.Uploaded/Failed based on the POST result so the frontend message stays accurate.

Copilot uses AI. Check for mistakes.
Comment on lines +1612 to +1623
// Add screenshots as separate comments (one per screenshot) so they
// don't blow up the 65K issue body limit. Each comment contains a
// base64 data URI in a collapsible <details> block with a marker
// that the process-screenshots GHA workflow can find and process.
if err == nil && len(validScreenshots) > 0 {
for i, dataURI := range validScreenshots {
commentBody := fmt.Sprintf(
"<!-- screenshot-base64:%d -->\n<details>\n<summary>Screenshot %d (processing...)</summary>\n\n```\n%s\n```\n\n</details>",
i+1, i+1, dataURI)
h.addIssueComment(number, commentBody, repoName)
}
log.Printf("[Feedback] Added %d screenshot comment(s) to issue #%d", len(validScreenshots), number)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot comments are posted synchronously and any failures are ignored (addIssueComment doesn’t return an error, and its result isn’t checked). This can both slow down the request (multiple GitHub API calls) and cause “success” responses even when no screenshot comments were created. Consider (1) capturing failures and reflecting them in ssResult, and (2) deciding whether comment posting should be async (e.g., goroutines with bounded concurrency) or must be best-effort but explicitly reflected back to the caller.

Copilot uses AI. Check for mistakes.
Comment on lines 13 to +26
on:
issues:
types: [opened]
issue_comment:
types: [created]

permissions:
contents: write
issues: write

jobs:
process-screenshots:
# Only run if the issue body contains our base64 screenshot marker
if: contains(github.event.issue.body, '<!-- screenshot-base64:')
process-screenshot:
# Only run on issue comments (not PR comments) with our marker
if: >
!github.event.issue.pull_request &&
contains(github.event.comment.body, '<!-- screenshot-base64:')
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow will run for any newly created issue comment containing the marker, and it has contents: write, meaning any user who can comment on issues could potentially cause arbitrary files to be committed to the repo. Add a hard gate to only process comments from a trusted actor (e.g., the bot/service account used by the backend token, repo members, or comments on issues created by the console) before decoding/committing.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +75
// Commit the image to the repo
const filePath = `.github/screenshots/${requestId}/screenshot-${index}.${ext}`;
console.log(`Uploading ${filePath}...`);

const { data } = await github.rest.repos.createOrUpdateFileContents({
owner: context.repo.owner,
repo: context.repo.repo,
path: filePath,
message: `Add screenshot ${index} for issue #${issueNumber}`,
content: b64Content,
committer: {
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createOrUpdateFileContents will fail with a 422 when updating an existing path unless sha is provided. Since filePath is deterministic (screenshot-{index}.{ext}), re-processing a screenshot (duplicate comment, retries, multiple submits on same issue) can hit this and replace the comment with an error. Consider using a unique filename (e.g., include commentId or a timestamp) or first fetching the existing file’s sha and passing it when updating.

Copilot uses AI. Check for mistakes.
@clubanderson
Copy link
Copy Markdown
Collaborator Author

🔄 Auto-Applying Copilot Code Review

Copilot code review found 3 code suggestion(s) and 4 general comment(s).

@copilot Please apply all of the following code review suggestions:

  • web/src/lib/imageCompression.ts (line 9): `* Compress an image data URI to fit within GitHub's issue comment limit.
  • S...`
  • web/src/components/feedback/FeedbackModal.tsx (line 178): let failedCompressionCount = 0 for (const s of screenshots) { cons...
  • web/src/components/feedback/FeatureRequestModal.tsx (line 349): let screenshotCompressionFailures = 0 for (const s of screenshots) { c...

Also address these general comments:

  • pkg/api/handlers/feedback.go (line 1585): screenshotUploadResult is now incremented based only on data-URI parsing, not on whether the screenshot comment was actu
  • pkg/api/handlers/feedback.go (line 1623): Screenshot comments are posted synchronously and any failures are ignored (addIssueComment doesn’t return an error, and
  • .github/workflows/process-screenshots.yml (line 26): This workflow will run for any newly created issue comment containing the marker, and it has contents: write, meaning
  • .github/workflows/process-screenshots.yml (line 75): createOrUpdateFileContents will fail with a 422 when updating an existing path unless sha is provided. Since filePath

Push all fixes in a single commit. Run cd web && npm run build && npm run lint before committing.


Auto-generated by copilot-review-apply workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants