Skip to content

🐛 Embed screenshots as base64 in issues; GHA processes into images#4327

Merged
clubanderson merged 1 commit intomainfrom
fix/screenshot-base64-embed
Apr 2, 2026
Merged

🐛 Embed screenshots as base64 in issues; GHA processes into images#4327
clubanderson merged 1 commit intomainfrom
fix/screenshot-base64-embed

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Fixes #4220 — screenshot uploads never worked for non-org-members because the Contents API requires push access to the repo.

Root cause

The backend uploaded screenshots via PUT /repos/{owner}/{repo}/contents/ which requires write/push access. Any user without org membership (i.e., every localhost user) got a 404. This was never a token scope issue — it was a fundamental permissions issue.

New approach (two-step)

  1. Backend embeds screenshots as base64 data URIs in the issue body inside collapsible <details> blocks with machine-readable markers:

    <!-- screenshot-base64:1 -->
    <details><summary>Screenshot 1 (processing...)</summary>
    data:image/png;base64,iVBOR...
    </details>
    

    No push access needed — just Issues write permission.

  2. GHA workflow (process-screenshots.yml) triggers on issues.opened:

    • Finds <!-- screenshot-base64:N --> markers
    • Decodes base64, commits images to .github/screenshots/ using the workflow's GITHUB_TOKEN (which has write access)
    • Replaces base64 blocks with rendered ![Screenshot](url) markdown
    • Updates the issue body

Other changes

  • Frontend messages: "will render as an image shortly" instead of "uploaded and attached"
  • Token permission warnings for Contents scope removed (no longer needed)
  • uploadScreenshotToGitHub function retained but no longer called from the issue creation path

Test plan

  • Submit feedback with screenshot on localhost (non-org-member token) → issue created with base64 <details> block
  • GHA workflow triggers on the new issue → base64 replaced with rendered image
  • Submit without screenshot → no <details> blocks, no workflow trigger
  • Invalid image format → yellow warning "invalid image format"
  • Multiple screenshots → each gets its own numbered marker and is processed

#4220)

The previous approach uploaded screenshots to .github/screenshots/ via the
GitHub Contents API, which requires push access to the repo. This meant
any user without write access (i.e., every non-org-member running localhost)
got a 404 on screenshot upload — the feature never worked for external users.

New approach:
- Backend embeds screenshots as base64 data URIs in collapsible <details>
  blocks with machine-readable markers (<!-- screenshot-base64:N -->)
- Issue is created with the base64 data inline — no push access needed
- New GHA workflow (process-screenshots.yml) triggers on issues.opened,
  finds the markers, decodes base64, commits images to .github/screenshots/
  using the workflow's GITHUB_TOKEN (which has write access), and replaces
  the base64 blocks with rendered ![Screenshot](url) markdown
- Frontend messages updated: "will render as an image shortly" instead of
  "uploaded and attached"
- Token permission warnings for Contents scope removed (no longer needed)

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Copilot AI review requested due to automatic review settings April 2, 2026 20:59
@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 962a26e
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69ced8b83eb9300008799d8e
😎 Deploy Preview https://deploy-preview-4327.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 52ad446 into main Apr 2, 2026
16 checks passed
@kubestellar-prow kubestellar-prow bot deleted the fix/screenshot-base64-embed branch April 2, 2026 20:59
@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

👋 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.

@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

This PR changes how screenshots are attached to feedback/feature-request GitHub issues so that non-org-members can submit screenshots without needing repo push access, by embedding base64 data URIs in the issue body and using a GitHub Actions workflow to commit images and rewrite the issue body to image markdown.

Changes:

  • Frontend: update screenshot success/failure messaging to reflect “processed shortly” behavior.
  • Backend: embed screenshots as base64 <details> blocks with machine-readable markers in the created issue body.
  • CI/Automation: add an issues.opened GitHub Actions workflow to decode embedded screenshots, commit them under .github/screenshots/, and update the issue body with rendered image links.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
web/src/components/feedback/FeedbackModal.tsx Updates UI messaging around screenshot attachment; removes token-permission guidance that assumed Contents API uploads.
web/src/components/feedback/FeatureRequestModal.tsx Updates UI messaging around screenshot attachment; trims unused GitHub token constants import.
pkg/api/handlers/feedback.go Replaces Contents API screenshot upload with base64 embedding blocks in the issue body.
.github/workflows/process-screenshots.yml New workflow to extract base64 screenshots from issues, commit images, and rewrite the issue body.

Comment on lines +1579 to 1586
// Validate the data URI format
parts := strings.SplitN(dataURI, ",", 2)
if len(parts) != 2 {
ssResult.Failed++
log.Printf("[Feedback] Failed to upload screenshot %d: %v", i+1, err)
if strings.Contains(err.Error(), "404") {
log.Printf("[Feedback] Hint: a 404 from the GitHub Contents API usually means the token lacks 'Contents: Read and write' permission. "+
"Ensure FEEDBACK_GITHUB_TOKEN has this scope for %s/%s.", repoOwner, repoName)
}
log.Printf("[Feedback] Screenshot %d: invalid data URI format", i+1)
continue
}
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.

The new screenshot “validation” only checks that the data URI contains a comma, then increments Uploaded. If the URI isn’t actually data:image/<type>;base64,... (or contains invalid base64), the issue will still report screenshots as “attached”, but the process-screenshots.yml regex won’t match / the workflow will fail to commit the image.

Consider validating that the header matches the exact format the workflow expects (including data:image/*;base64,) and validating/normalizing base64 (e.g., padding + decode check like uploadScreenshotToGitHub already does) before counting it as uploaded/embedded.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +21
# Only run if the issue body contains our base64 screenshot marker
if: contains(github.event.issue.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 grants contents: write and runs on issues.opened. That means anyone who can open an issue (including first-time external users) can cause the workflow to commit arbitrary blobs into the repo under .github/screenshots/ by including the marker/base64 block, effectively escalating “issue write” into “repo write”.

Please add a trust gate (e.g., only process issues opened by a specific bot/service account, or require author_association to be MEMBER/OWNER, or verify an HMAC/signature emitted by the backend) and/or move storage to a safer target (separate repo/branch) to avoid unintended write access and repo-bloat abuse.

Suggested change
# Only run if the issue body contains our base64 screenshot marker
if: contains(github.event.issue.body, '<!-- screenshot-base64:')
# Only run for trusted issue authors when the issue body contains our
# base64 screenshot marker. This prevents arbitrary repository writes from
# untrusted external users who can open issues.
if: >
contains(github.event.issue.body, '<!-- screenshot-base64:') &&
(
github.event.issue.author_association == 'OWNER' ||
github.event.issue.author_association == 'MEMBER' ||
github.event.issue.author_association == 'COLLABORATOR'
)

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +43
// Find all screenshot-base64 markers
const markerRegex = /<!-- screenshot-base64:(\d+) -->\n<details>\n<summary>Screenshot \d+ \(processing\.\.\.\)<\/summary>\n\n```\n(data:image\/[^;]+;base64,[A-Za-z0-9+/=\n]+)\n```\n\n<\/details>/g;

let match;
const screenshots = [];
while ((match = markerRegex.exec(body)) !== null) {
screenshots.push({
fullMatch: match[0],
index: parseInt(match[1]),
dataUri: match[2].replace(/\n/g, ''),
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.

markerRegex is extremely strict (exact newlines, exact <summary> text, and a restricted base64 character class). Small formatting changes on the backend, CRLF normalization, or URL-safe base64 (-/_) would prevent matches and leave large base64 blobs stuck in the issue body.

Consider making the parsing more resilient: locate each <!-- screenshot-base64:N --> marker first, then parse the following fenced code block with more flexible newline handling (\r?\n) and a broader base64 matcher, instead of matching the entire <details> block verbatim.

Suggested change
// Find all screenshot-base64 markers
const markerRegex = /<!-- screenshot-base64:(\d+) -->\n<details>\n<summary>Screenshot \d+ \(processing\.\.\.\)<\/summary>\n\n```\n(data:image\/[^;]+;base64,[A-Za-z0-9+/=\n]+)\n```\n\n<\/details>/g;
let match;
const screenshots = [];
while ((match = markerRegex.exec(body)) !== null) {
screenshots.push({
fullMatch: match[0],
index: parseInt(match[1]),
dataUri: match[2].replace(/\n/g, ''),
// Find all screenshot-base64 markers, then parse the following fenced
// code block more flexibly so minor formatting changes do not prevent
// cleanup of embedded base64 content.
const markerRegex = /<!-- screenshot-base64:(\d+) -->/g;
const fencedDataUriRegex = /```[^\r\n]*\r?\n\s*(data:image\/[^;]+;base64,[A-Za-z0-9+/_=\r\n-]+)\s*\r?\n```/;
let match;
const screenshots = [];
while ((match = markerRegex.exec(body)) !== null) {
const markerStart = match.index;
const markerEnd = markerRegex.lastIndex;
const remainingBody = body.slice(markerEnd);
const fencedMatch = remainingBody.match(fencedDataUriRegex);
if (!fencedMatch || fencedMatch.index === undefined) {
continue;
}
const fencedStart = markerEnd + fencedMatch.index;
const fencedEnd = fencedStart + fencedMatch[0].length;
const detailsCloseIndex = body.indexOf('</details>', fencedEnd);
const fullMatchEnd = detailsCloseIndex === -1
? fencedEnd
: detailsCloseIndex + '</details>'.length;
screenshots.push({
fullMatch: body.slice(markerStart, fullMatchEnd),
index: parseInt(match[1], 10),
dataUri: fencedMatch[1].replace(/[\r\n]/g, ''),

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +84
// Decode base64 to verify it's valid
const content = b64Content;

// Commit the image to the repo
const filePath = `.github/screenshots/${requestId}/screenshot-${ss.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 ${ss.index} for issue #${issueNumber}`,
content: content,
committer: {
name: 'github-actions[bot]',
email: '41898282+github-actions[bot]@users.noreply.github.com',
},
});
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 script comment says “Decode base64 to verify it's valid”, but the code never decodes/validates it (const content = b64Content). If the input is malformed or missing padding, createOrUpdateFileContents will fail; also there’s no size limit, so a large base64 payload could cause excessive repo growth.

Suggested fixes: actually validate/normalize base64 (e.g., decode with Buffer.from(..., 'base64'), enforce a max byte size, then re-encode to base64 for the API), and consider handling reruns gracefully (if the file already exists, fetch its sha before calling createOrUpdateFileContents).

Copilot uses AI. Check for mistakes.
import { FETCH_DEFAULT_TIMEOUT_MS, COPY_FEEDBACK_TIMEOUT_MS } from '../../lib/constants'
import { FEEDBACK_UPLOAD_TIMEOUT_MS } from '../../lib/constants/network'
import { GITHUB_TOKEN_MANAGE_URL, GITHUB_TOKEN_FINE_GRAINED_PERMISSIONS, GITHUB_TOKEN_CLASSIC_SCOPE } from '../../lib/constants/github-token'
// github-token constants no longer needed here — screenshots are embedded as 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.

There are still comments in this file that describe the old “upload screenshots to GitHub and embed as markdown images” behavior, but screenshots are now embedded as base64 in the issue body and later processed by GitHub Actions. Please update the surrounding documentation/comments (including the top-of-file header comment) to match the new two-step flow to avoid misleading future maintainers.

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

🔄 Auto-Applying Copilot Code Review

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

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

  • .github/workflows/process-screenshots.yml (line 21): `# Only run for trusted issue authors when the issue body contains our

    base...`

  • .github/workflows/process-screenshots.yml (line 43): // Find all screenshot-base64 markers, then parse the following fenced ...

Also address these general comments:

  • pkg/api/handlers/feedback.go (line 1586): The new screenshot “validation” only checks that the data URI contains a comma, then increments Uploaded. If the URI i
  • .github/workflows/process-screenshots.yml (line 84): The script comment says “Decode base64 to verify it's valid”, but the code never decodes/validates it (`const content =
  • web/src/components/feedback/FeedbackModal.tsx (line 24): There are still comments in this file that describe the old “upload screenshots to GitHub and embed as markdown images”

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.

Testing image attachment

3 participants