Skip to content

Add QR codes to deploy preview links in PR comments#1530

Draft
Janpot wants to merge 3 commits into
masterfrom
dashboard-qr-code-endpoint
Draft

Add QR codes to deploy preview links in PR comments#1530
Janpot wants to merge 3 commits into
masterfrom
dashboard-qr-code-endpoint

Conversation

@Janpot

@Janpot Janpot commented Jun 10, 2026

Copy link
Copy Markdown
Member

Closes #261. Revives mui/material-ui#45078, now that the PR comment is generated by the dashboard.

Adds a GET /api/qr-code?url=...&sig=... endpoint to the code-infra-dashboard and wraps each deploy preview link in the PR comment in a collapsible QR code, for opening preview pages on a phone.

  • Security: target URLs are signed with HMAC-SHA256 (truncated to 128 bits) at comment-generation time and verified by the endpoint with a constant-time compare, so it can't be used as a general-purpose QR generator. The key is a dedicated QR_CODE_SECRET, generated by Render (generateValue: true), so it can be rotated independently of other app secrets.
  • Caching: responses are deterministic per URL and served with Cache-Control: public, max-age=31536000, immutable. GitHub proxies comment images through camo, which caches based on these headers, so repeat views never hit the server. Error responses are no-store.
  • Bandwidth: SVG output with error correction level L and a small quiet zone — ~1.4 kB raw, a few hundred bytes gzipped, crisp at any scale.

Without QR_CODE_SECRET configured (e.g. local dev), the report falls back to plain links and the endpoint returns 503.

Also fixes doc preview links being joined with a double slash (netlify.app//material-ui/...) by constructing them with new URL().

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-1530--mui-internal.netlify.app/

Bundle size

Total Size Change: 0B(0.00%) - Total Gzip Change: 0B(0.00%)
Files: 63 total (0 added, 0 removed, 0 changed)

Show details for 63 more bundles

@mui/internal-docs-infra/abstractCreateDemoparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/abstractCreateDemoClientparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/abstractCreateStreamparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/abstractCreateTypesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/ChunkProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/cliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeControllerContextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeExternalsContextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeHighlighterparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeHighlighter/errorsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeHighlighter/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CoordinatedLazyparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CoordinatedLazy/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createDemoDataparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createDemoData/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createSitemapparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createSitemap/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCodeparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCodeWindowparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCoordinatedparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCopierparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useDemoparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useErrorsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useLocalStorageStateparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/usePreferenceparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useScrollAnchorparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useSearchparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useSearch/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useStreamparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useStream/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useTypeparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useTypesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useUrlHashStateparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/withDocsInfraparsed: 0B(0.00%) gzip: 0B(0.00%)
addLineGuttersparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeHighlighterChunkparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeHighlighterClientparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeInitialSourceLoaderparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeSourceLoaderparsed: 0B(0.00%) gzip: 0B(0.00%)
createFrameparsed: 0B(0.00%) gzip: 0B(0.00%)
createParseSourceWorkerClientparsed: 0B(0.00%) gzip: 0B(0.00%)
EditingEngineparsed: 0B(0.00%) gzip: 0B(0.00%)
embedTransformsparsed: 0B(0.00%) gzip: 0B(0.00%)
enhanceCodeEmphasisparsed: 0B(0.00%) gzip: 0B(0.00%)
findExpandingRangesparsed: 0B(0.00%) gzip: 0B(0.00%)
getHastTextContentparsed: 0B(0.00%) gzip: 0B(0.00%)
grammarLoadersparsed: 0B(0.00%) gzip: 0B(0.00%)
grammarsparsed: 0B(0.00%) gzip: 0B(0.00%)
isFrameSpanparsed: 0B(0.00%) gzip: 0B(0.00%)
loadIsomorphicCodeVariantparsed: 0B(0.00%) gzip: 0B(0.00%)
parseSourceparsed: 0B(0.00%) gzip: 0B(0.00%)
source.cssparsed: 0B(0.00%) gzip: 0B(0.00%)
source.jsparsed: 0B(0.00%) gzip: 0B(0.00%)
source.jsonparsed: 0B(0.00%) gzip: 0B(0.00%)
source.mdxparsed: 0B(0.00%) gzip: 0B(0.00%)
source.shellparsed: 0B(0.00%) gzip: 0B(0.00%)
source.tsparsed: 0B(0.00%) gzip: 0B(0.00%)
source.tsxparsed: 0B(0.00%) gzip: 0B(0.00%)
source.yamlparsed: 0B(0.00%) gzip: 0B(0.00%)
text.html.basicparsed: 0B(0.00%) gzip: 0B(0.00%)
text.mdparsed: 0B(0.00%) gzip: 0B(0.00%)
TransformEngineparsed: 0B(0.00%) gzip: 0B(0.00%)

Details of bundle changes

Performance

Total duration: 18.73 ms +1.79 ms(+10.5%) | Renders: 4 (+0) | Paint: 76.12 ms +0.98 ms(+1.3%)

Test Duration Renders
DataGrid mount with paint timing 3.08 ms 🔺+0.55 ms(+21.6%) 1 (+0)

2 tests within noise — details


Check out the code infra dashboard for more information about this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds signed, cacheable QR codes for deploy preview links in the CI PR comment generated by the code-infra dashboard, making it easier to open preview pages on mobile while keeping the endpoint from becoming a general-purpose QR generator.

Changes:

  • Introduces /api/qr-code?url=...&sig=... that verifies an HMAC signature and returns an SVG QR code with long-term caching headers.
  • Updates deploy preview report output to wrap preview/doc links in a collapsible <details> block that includes the QR image (with plain-link fallback when QR_CODE_SECRET is unset).
  • Fixes doc preview link construction to avoid double slashes by using new URL(docPath, previewUrl).

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
render.yaml Adds QR_CODE_SECRET env var generation for dashboard deployments.
apps/code-infra-dashboard/package.json Adds qrcode + @types/qrcode dependencies for SVG generation.
pnpm-lock.yaml Locks the new QR code dependencies and transitive graph.
apps/code-infra-dashboard/src/lib/qrCode.ts Implements signing/verifying and SVG QR rendering helpers.
apps/code-infra-dashboard/src/lib/qrCode.test.ts Unit tests for signing, verification behavior, and SVG generation.
apps/code-infra-dashboard/src/lib/ciReports/deployPreviewReport.ts Wraps deploy preview links in collapsible QR blocks; fixes doc URL joining.
apps/code-infra-dashboard/src/lib/ciReports/deployPreviewReport.test.ts Tests QR-wrapped output, signed URL embedding, and fallback behavior.
apps/code-infra-dashboard/app/api/qr-code/route.ts New signed QR code endpoint with caching + security headers.
apps/code-infra-dashboard/app/api/qr-code/route.test.ts Route tests for success + error paths (missing params, invalid sig, non-https, etc.).
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +10 to +21
/**
* Formats a link with a collapsible QR code for opening it on a phone.
* Falls back to a plain markdown link when no signing key is configured.
* Single-line HTML so it renders correctly inside markdown list items.
*/
function formatLinkWithQr(label: string, url: string): string {
const qrCodeUrl = signQrCodeUrl(url);
if (!qrCodeUrl) {
return `[${label}](${url})`;
}
return `<details><summary><a href="${url}">${label}</a></summary><img src="${qrCodeUrl}" width="150" alt="QR code for ${label}"></details>`;
}
Comment on lines +12 to +21
const url = request.nextUrl.searchParams.get('url');
const signature = request.nextUrl.searchParams.get('sig');

if (!url || !signature) {
return errorResponse('Missing url or sig query parameter', 400);
}

if (url.length > QR_CODE_MAX_URL_LENGTH) {
return errorResponse('url query parameter is too long', 400);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[docs-infra] Support QR code on deploy preview to ease mobile test

3 participants