Skip to content

Commit adb6c17

Browse files
ankur-archclaudeAmanVarshney01
authored
docs(compute): Deploy Button page and generator (repository-url) (#8116)
* docs(compute): add Deploy Button page and generator Adds a Deploy Button reference page under Compute integrations: what the button does, supported templates, URL format with attribution parameters, troubleshooting, and copy-paste Markdown/HTML snippets. Includes an interactive generator component and a hosted "Deploy with Prisma" SVG badge for third-party READMEs to hot-link. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): make the Deploy Button repository-first The button's contract is now a repository URL: any public GitHub repository with a root prisma.compute.json, a package.json, and a lockfile is deployable — no registration or catalog entry. The page documents the minimal repository contract and the /new/clone?repository-url=… format, and the generator takes a repository URL plus optional project-name and attribution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docs): correct Deploy Button contract details from Console audit - prisma.compute.json example uses the real schema (app.httpPort, not top-level name/port — the old example failed validation verbatim). - Document the validation limits (file count/size caps, no workflows, no symlinks/submodules, single-app, region key rejected). - Match the Console's actual copy for the name-conflict error, the 15-minute authorization window, commit display and pinning behavior, the workspace picker, and the private-by-default visibility. - Qualify push-to-recover advice by whether the project was created. - Generator: project-name field shows its own validation hint, copy buttons get distinct accessible names, HTML snippet escapes ampersands in the href. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): env-var support in the Deploy Button page and generator - Fix the main button: a literal anchor + img instead of the markdown image, which the docs pipeline wrapped in ImageZoom and made unclickable - Remove the early-access note - Document the env / env-example-<NAME> URL parameters and add an Environment variables section (names in the URL, values prompted in Console, DATABASE_URL provisioned automatically) - Generator: rows for required env-var names + safe example values, emitted on the URL, Markdown, and HTML outputs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): document compute-config env vars; validate examples like the Console The generator now rejects example values with control characters, the same rule the Console applies, so a pasted tab fails for the author instead of for their users. The env-var section documents the second source of prompts: app.env.vars entries in prisma.compute.json appear on the deploy form with the committed value as the prefill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): lead the Deploy Button page with the generator; harden and simplify it Restructure the page pyramid-style: the generator now sits directly under the intro, with the repository contract, URL reference, and background sections following in order of reader need. Requirements fold into How it works and the static snippets fold into URL format. Generator hardening: utm fields are validated (100-char cap, no control characters), example values reject bidi and zero-width characters that could visually spoof a README, and the HTML snippet escaper also covers < > and single quotes. Simplification: snippets render through eclipse's CodeBlock (native docs styling plus its copy button), the four labeled inputs collapse into one Field component with aria-invalid styling, and the URL is built in plain code instead of a useMemo with derived deps. The env-var rules comment now points at the canonical implementation in pdp-control-plane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): state that the Deploy Button requires the JSON config form Connected repositories accept prisma.compute.ts, but the /new/clone validation only accepts a root prisma.compute.json. Say so explicitly so authors who used the typed config know why validation rejects their repository. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): deploy-button review fixes - Say plainly that links carry names and public example values, never secrets (the env-example paragraph contradicted the earlier rule). - Reject duplicate variable names in the generator instead of silently keeping the first example. - Make the repository contract actionable: show framework and entry in the minimal config, define the project requirement as JavaScript or TypeScript, point at prisma-cli init for generating the file, and mark the env.vars snippet as a fragment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(compute): note that env var names can't start with a number The Console regex (/^[A-Z_][A-Z0-9_]*$/) rejects a leading digit; the prose said only "uppercase letters, numbers, and underscores". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docs): align deploy button with Composer (#8163) * docs(compute): remove stale Composer access warning * docs(compute): focus Deploy Button guide on users * docs(compute): clarify Deploy Button user flow * docs(compute): update Deploy Button branding * docs(compute): balance Deploy Button spacing --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Aman Varshney <amanvarshney.work@gmail.com>
1 parent e7abb29 commit adb6c17

5 files changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
title: Deploy Button
3+
description: Add a Deploy with Prisma button that copies a public Composer repository and starts a Composer-managed deployment.
4+
url: /compute/deploy-button
5+
metaTitle: Deploy Button | Prisma Compute
6+
metaDescription: Let users copy your public Composer repository and deploy its declared resources with Prisma.
7+
---
8+
9+
The Deploy Button starts a guided deployment from a compatible public GitHub repository. Prisma copies the source into a new repository under the chosen GitHub account or organization and adds a managed GitHub Actions workflow. That workflow runs Composer, which creates the Prisma project and provisions the resources declared by the repository.
10+
11+
<img
12+
src="/docs/img/deploy-button.svg"
13+
alt="Deploy with Prisma"
14+
width="172"
15+
height="36"
16+
/>
17+
18+
Add the button to a README, blog post, or website so other users can deploy their own copy.
19+
20+
## Create your button
21+
22+
Enter your repository URL and copy the generated snippet:
23+
24+
<DeployButtonGenerator />
25+
26+
## Repository requirements
27+
28+
Before publishing the button, make sure the source repository:
29+
30+
- is a public GitHub repository;
31+
- has a Composer config and module at its root;
32+
- default-exports a Composer module whose name is a string literal;
33+
- has a root `package.json` with a `build` script, or uses Nx build targets;
34+
- has one supported root lockfile: `bun.lock`, `package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`; and
35+
- declares every required service and resource in its Composer module.
36+
37+
The repository can contain up to 100 files and 25 MB in total, with no file larger than 5 MB. Symlinks, Git submodules, and custom GitHub Actions workflows are not supported.
38+
39+
See [Getting started with Composer](/composer/getting-started) to prepare a repository.
40+
41+
## URL options
42+
43+
A Deploy Button links to:
44+
45+
```text title="Deploy Button URL"
46+
https://console.prisma.io/new/clone?repository-url=<your repository URL>
47+
```
48+
49+
| Parameter | Required | Description |
50+
| ---------------- | -------- | ----------- |
51+
| `repository-url` | Yes | Public GitHub repository in the form `https://github.com/owner/repo`. URL-encode it when composing the link. |
52+
| `project-name` | No | Prefills the destination repository name. The user can change it. |
53+
| `utm_source` | No | Records where the deployment link was published, such as `github-readme`. |
54+
55+
Deploy Button URLs do not accept environment variables. The first deployment must not depend on secrets that Composer cannot provide. Add any other variables to the project after deployment, then redeploy.
56+
57+
## How it works
58+
59+
1. The user chooses a workspace, a GitHub account or organization, a repository name, and its visibility.
60+
2. Prisma copies the source into a new repository under that account or organization and adds the deployment workflow.
61+
3. The workflow runs Composer, which creates the Prisma project, provisions its resources, and deploys the app. Future pushes run the workflow again.

apps/docs/content/docs/compute/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"domains",
1717
"---Integrations---",
1818
"github",
19+
"deploy-button",
1920
"alchemy",
2021
"---Reference---",
2122
"configuration",
Lines changed: 18 additions & 0 deletions
Loading
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"use client";
2+
import { useMemo, useState } from "react";
3+
import { CodeBlock, Pre } from "@prisma/eclipse";
4+
import { cn } from "@prisma-docs/ui/lib/cn";
5+
import { withDocsBasePath } from "@/lib/urls";
6+
7+
const CONSOLE_CLONE_URL = "https://console.prisma.io/new/clone";
8+
const BUTTON_IMAGE_PATH = "/img/deploy-button.svg";
9+
const BUTTON_IMAGE_URL = `https://www.prisma.io/docs${BUTTON_IMAGE_PATH}`;
10+
const BUTTON_ALT = "Deploy with Prisma";
11+
const BUTTON_WIDTH = 172;
12+
const BUTTON_HEIGHT = 36;
13+
14+
const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
15+
const REPO_PATTERN = /^[A-Za-z0-9._-]{1,100}$/;
16+
const PROJECT_NAME_PATTERN = REPO_PATTERN;
17+
const MAX_UTM_LENGTH = 100;
18+
19+
// Generated values are commonly pasted into a README, so reject characters
20+
// that could hide or visually reorder its content.
21+
const UNSAFE_CHARACTERS =
22+
// eslint-disable-next-line no-control-regex
23+
/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]/;
24+
25+
function utmValid(value: string): boolean {
26+
const trimmed = value.trim();
27+
return trimmed.length <= MAX_UTM_LENGTH && !UNSAFE_CHARACTERS.test(trimmed);
28+
}
29+
30+
function parseRepositoryUrl(raw: string): { owner: string; repo: string } | null {
31+
const value = raw.trim();
32+
if (value.length === 0 || value.length > 300) return null;
33+
let url: URL;
34+
try {
35+
url = new URL(value.includes("://") ? value : `https://${value}`);
36+
} catch {
37+
return null;
38+
}
39+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port) return null;
40+
if (url.username || url.password || url.search || url.hash) return null;
41+
const segments = url.pathname.split("/").filter(Boolean);
42+
if (segments.length !== 2) return null;
43+
const owner = segments[0];
44+
let repo = segments[1];
45+
if (repo.endsWith(".git")) repo = repo.slice(0, -4);
46+
if (!OWNER_PATTERN.test(owner) || !REPO_PATTERN.test(repo)) return null;
47+
if (repo === "." || repo === "..") return null;
48+
return { owner, repo };
49+
}
50+
51+
function escapeHtmlAttribute(value: string): string {
52+
return value
53+
.replaceAll("&", "&amp;")
54+
.replaceAll('"', "&quot;")
55+
.replaceAll("'", "&#39;")
56+
.replaceAll("<", "&lt;")
57+
.replaceAll(">", "&gt;");
58+
}
59+
60+
function Snippet({ label, value }: { label: string; value: string }) {
61+
return (
62+
<CodeBlock title={label} className="my-0">
63+
<Pre>
64+
<code>{value}</code>
65+
</Pre>
66+
</CodeBlock>
67+
);
68+
}
69+
70+
const fieldClassName =
71+
"w-full rounded-lg border bg-fd-background px-3 py-2 text-sm text-fd-foreground outline-none focus-visible:ring-2 focus-visible:ring-fd-ring aria-invalid:border-red-500/70";
72+
73+
function Field({
74+
label,
75+
hintId,
76+
hint,
77+
valid,
78+
span2 = false,
79+
optional = true,
80+
value,
81+
placeholder,
82+
onChange,
83+
}: {
84+
label: string;
85+
hintId: string;
86+
hint: string;
87+
valid: boolean;
88+
span2?: boolean;
89+
optional?: boolean;
90+
value: string;
91+
placeholder: string;
92+
onChange: (value: string) => void;
93+
}) {
94+
return (
95+
<label className={cn("flex flex-col gap-1.5", span2 && "sm:col-span-2")}>
96+
<span className="text-sm font-medium">
97+
{label}
98+
{optional ? (
99+
<span className="font-normal text-fd-muted-foreground"> (optional)</span>
100+
) : null}
101+
</span>
102+
<input
103+
className={fieldClassName}
104+
placeholder={placeholder}
105+
value={value}
106+
required={!optional}
107+
aria-invalid={!valid}
108+
aria-describedby={valid ? undefined : hintId}
109+
onChange={(event) => onChange(event.target.value)}
110+
/>
111+
{!valid ? (
112+
<span id={hintId} className="text-xs text-fd-muted-foreground">
113+
{hint}
114+
</span>
115+
) : null}
116+
</label>
117+
);
118+
}
119+
120+
export function DeployButtonGenerator() {
121+
const [repositoryUrl, setRepositoryUrl] = useState("");
122+
const [projectName, setProjectName] = useState("");
123+
const [utmSource, setUtmSource] = useState("");
124+
125+
const parsed = useMemo(() => parseRepositoryUrl(repositoryUrl), [repositoryUrl]);
126+
const repositoryUrlValid = repositoryUrl.trim() === "" || parsed !== null;
127+
const trimmedProjectName = projectName.trim();
128+
const projectNameValid =
129+
trimmedProjectName === "" ||
130+
(PROJECT_NAME_PATTERN.test(trimmedProjectName) &&
131+
trimmedProjectName !== "." &&
132+
trimmedProjectName !== "..");
133+
const utmSourceValid = utmValid(utmSource);
134+
135+
let url: string | null = null;
136+
if (parsed && projectNameValid && utmSourceValid) {
137+
const search = new URLSearchParams();
138+
search.set("repository-url", `https://github.com/${parsed.owner}/${parsed.repo}`);
139+
if (trimmedProjectName) search.set("project-name", trimmedProjectName);
140+
if (utmSource.trim()) search.set("utm_source", utmSource.trim());
141+
url = `${CONSOLE_CLONE_URL}?${search.toString()}`;
142+
}
143+
144+
return (
145+
<div className="not-prose flex flex-col gap-5 rounded-xl border p-5">
146+
<div className="grid gap-4 sm:grid-cols-2">
147+
<Field
148+
label="Repository URL"
149+
optional={false}
150+
span2
151+
placeholder="https://github.com/owner/repo"
152+
value={repositoryUrl}
153+
valid={repositoryUrlValid}
154+
hintId="deploy-button-repository-url-hint"
155+
hint="Enter a public GitHub repository URL like https://github.com/owner/repo."
156+
onChange={setRepositoryUrl}
157+
/>
158+
<Field
159+
label="Destination repository name"
160+
placeholder="my-app"
161+
value={projectName}
162+
valid={projectNameValid}
163+
hintId="deploy-button-project-name-hint"
164+
hint="Use 1–100 letters, numbers, dots, dashes, or underscores."
165+
onChange={setProjectName}
166+
/>
167+
<Field
168+
label="Attribution source"
169+
placeholder="github-readme"
170+
value={utmSource}
171+
valid={utmSourceValid}
172+
hintId="deploy-button-utm-source-hint"
173+
hint={`Use a short label of up to ${MAX_UTM_LENGTH} visible characters.`}
174+
onChange={setUtmSource}
175+
/>
176+
</div>
177+
178+
{url ? (
179+
<>
180+
<div className="flex items-center gap-3 rounded-lg border border-dashed p-4">
181+
<span className="text-sm text-fd-muted-foreground">Preview:</span>
182+
<a href={url} target="_blank" rel="noreferrer">
183+
{/* eslint-disable-next-line @next/next/no-img-element */}
184+
<img
185+
src={withDocsBasePath(BUTTON_IMAGE_PATH)}
186+
alt={BUTTON_ALT}
187+
width={BUTTON_WIDTH}
188+
height={BUTTON_HEIGHT}
189+
/>
190+
</a>
191+
</div>
192+
<Snippet label="URL" value={url} />
193+
<Snippet label="Markdown" value={`[![${BUTTON_ALT}](${BUTTON_IMAGE_URL})](${url})`} />
194+
<Snippet
195+
label="HTML"
196+
value={`<a href="${escapeHtmlAttribute(url)}"><img src="${BUTTON_IMAGE_URL}" alt="${BUTTON_ALT}" width="${BUTTON_WIDTH}" height="${BUTTON_HEIGHT}" /></a>`}
197+
/>
198+
</>
199+
) : (
200+
<p className="text-sm text-fd-muted-foreground">
201+
Enter your repository's GitHub URL to generate the button. The repository must be public
202+
and contain a Composer config and module at its root.
203+
</p>
204+
)}
205+
</div>
206+
);
207+
}

apps/docs/src/mdx-components.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx";
22
import { Youtube } from "@prisma-docs/ui/components/youtube";
33
import { APIPage } from "@/components/api-page";
44
import { ConceptAnimation } from "@/components/concept-animation";
5+
import { DeployButtonGenerator } from "@/components/deploy-button-generator";
56
import {
67
AgentPrompt,
78
CliCallout,
@@ -125,6 +126,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
125126
Bucket: BucketIcon,
126127
CliCallout,
127128
ConceptAnimation,
129+
DeployButtonGenerator,
128130
IconGrid,
129131
IconLink,
130132
ModalRow,

0 commit comments

Comments
 (0)