Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/submit-on-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ jobs:
DEBUG: "1"
PR_BODY_FILE: /tmp/pr-body.txt
SESSION_TOKEN: ${{ secrets.SESSION_TOKEN }}
FRAMER_ADMIN_SECRET: ${{ secrets.FRAMER_ADMIN_SECRET }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_ERROR_WEBHOOK_URL: ${{ secrets.SLACK_ERROR_WEBHOOK_URL }}
RETOOL_URL: ${{ secrets.RETOOL_URL }}
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/submit-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ on:
SESSION_TOKEN:
description: 'Framer session cookie'
required: true
FRAMER_ADMIN_SECRET:
description: 'Framer admin API key'
required: true
SLACK_WEBHOOK_URL:
description: 'Slack webhook URL for notifications'
required: false
Expand Down Expand Up @@ -146,7 +143,6 @@ jobs:
CHANGELOG: ${{ inputs.changelog }}
PR_BODY: ${{ inputs.pr_body }}
SESSION_TOKEN: ${{ secrets.SESSION_TOKEN }}
FRAMER_ADMIN_SECRET: ${{ secrets.FRAMER_ADMIN_SECRET }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_ERROR_WEBHOOK_URL: ${{ secrets.SLACK_ERROR_WEBHOOK_URL }}
RETOOL_URL: ${{ secrets.RETOOL_URL }}
Expand Down
4 changes: 0 additions & 4 deletions scripts/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,25 @@ export const EnvSchema = v.object({
DRY_RUN: BooleanEnvSchema,
REPO_ROOT: v.optional(v.string()),
SESSION_TOKEN: v.pipe(v.string(), v.minLength(1)),
FRAMER_ADMIN_SECRET: v.pipe(v.string(), v.minLength(1)),
})

export type FramerEnv = v.InferOutput<typeof FramerEnvSchema>
export type Environment = v.InferOutput<typeof EnvSchema>

export interface EnvironmentUrls {
apiBase: string
creatorsApiBase: string
framerAppUrl: string
marketplaceBaseUrl: string
}

export const ENVIRONMENT_URLS: Record<FramerEnv, EnvironmentUrls> = {
production: {
apiBase: "https://api.framer.com",
creatorsApiBase: "https://marketplace.framer.com",
framerAppUrl: "https://framer.com",
marketplaceBaseUrl: "https://framer.com/marketplace",
},
development: {
apiBase: "https://api.development.framer.com",
creatorsApiBase: "https://marketplace.development.framer.com",
framerAppUrl: "https://development.framer.com",
marketplaceBaseUrl: "https://marketplace.development.framer.com/marketplace",
},
Expand Down
80 changes: 64 additions & 16 deletions scripts/lib/framer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const SubmissionResponseSchema = v.object({
})
export type SubmissionResponse = v.InferOutput<typeof SubmissionResponseSchema>

/** The Creators Service wraps every response, so the release details sit under `data`. */
const ReleaseResponseSchema = v.object({
data: SubmissionResponseSchema,
})

export const FramerJsonSchema = v.object({
id: v.string(),
name: v.string(),
Expand Down Expand Up @@ -111,43 +116,86 @@ export function loadFramerJsonFile(pluginPath: string): FramerJson {
return framerJson
}

/**
* Two calls, because the zip goes to the plugins API and only the resulting
* version id goes to the marketplace. The version is left pending for a human
* to approve.
*/
export async function submitPlugin(
zipFilePath: string,
plugin: Plugin,
env: Environment,
changelog: string
): Promise<SubmissionResponse> {
if (!env.SESSION_TOKEN || !env.FRAMER_ADMIN_SECRET) {
throw new Error("Session token and Framer admin secret are required for submission")
}
const accessToken = await getAccessToken(env)
const releaseNotes = await changelogToHtml(changelog)

const url = `${getURL(env, "creatorsApiBase")}/api/admin/plugin/${plugin.id}/versions/`
const versionId = await uploadPluginVersion(zipFilePath, plugin, env, accessToken, releaseNotes)
const result = await submitToMarketplace(plugin, env, accessToken, versionId)

log.info(`Submitting to: ${url}`)
log.success(`Submitted! Version: ${result.version}`)

const zipBuffer = readFileSync(zipFilePath)
const blob = new Blob([zipBuffer], { type: "application/zip" })
return result
}

async function uploadPluginVersion(
zipFilePath: string,
plugin: Plugin,
env: Environment,
accessToken: string,
releaseNotes: string
): Promise<string> {
const url = `${getURL(env, "apiBase")}/site/v1/plugins/${plugin.id}/versions`

log.info(`Uploading to: ${url}`)

const zipBuffer = readFileSync(zipFilePath)
const formData = new FormData()
formData.append("file", blob, "plugin.zip")
formData.append("content", await changelogToHtml(changelog))
formData.append("file", new Blob([zipBuffer], { type: "application/zip" }), "plugin.zip")
formData.append("releaseNotes", releaseNotes)

const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
body: formData,
})

if (!response.ok) {
const errorText = await response.text()
throw new Error(`Plugin upload failed: ${response.status} ${response.statusText}\n${errorText}`)
}

const version = v.parse(PluginVersionSchema, await response.json())

return version.id
}

async function submitToMarketplace(
plugin: Plugin,
env: Environment,
accessToken: string,
versionId: string
): Promise<SubmissionResponse> {
const url = `${getURL(env, "apiBase")}/creators/v1/resources/plugin/${plugin.id}/release`

log.info(`Submitting to: ${url}`)

const response = await fetch(url, {
method: "POST",
headers: {
Cookie: `session=${env.SESSION_TOKEN}`,
Authorization: `Bearer ${env.FRAMER_ADMIN_SECRET}`,
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"x-requested-by": "plugin-release-action",
},
body: formData,
body: JSON.stringify({ versionId }),
})

if (!response.ok) {
const errorText = await response.text()
throw new Error(`API submission failed: ${response.status} ${response.statusText}\n${errorText}`)
throw new Error(`Marketplace submission failed: ${response.status} ${response.statusText}\n${errorText}`)
}

const result = v.parse(SubmissionResponseSchema, await response.json())
log.success(`Submitted! Version: ${result.version}`)
const body = v.parse(ReleaseResponseSchema, await response.json())

return result
return body.data
}
2 changes: 1 addition & 1 deletion scripts/submit-on-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* REPO_ROOT - Root of the git repository (optional, defaults to parent of scripts/)
*
* Plus all environment variables required by submit-plugin.ts:
* SESSION_TOKEN, FRAMER_ADMIN_SECRET, SLACK_WEBHOOK_URL, etc.
* SESSION_TOKEN, SLACK_WEBHOOK_URL, etc.
*/

import { execSync } from "node:child_process"
Expand Down
3 changes: 1 addition & 2 deletions scripts/submit-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* CHANGELOG - Changelog text (required unless PR_BODY is set)
* PR_BODY - Full PR body — changelog will be extracted (alternative to CHANGELOG)
* SESSION_TOKEN - Framer session cookie (required unless DRY_RUN)
* FRAMER_ADMIN_SECRET - Framer admin API key (required unless DRY_RUN)
* SLACK_WEBHOOK_URL - Slack workflow webhook for success notifications (optional)
* SLACK_ERROR_WEBHOOK_URL - Slack workflow webhook for error notifications (optional)
* RETOOL_URL - Retool dashboard URL for Slack notifications (optional)
Expand Down Expand Up @@ -50,7 +49,7 @@ async function main(): Promise<void> {
try {
log.info(`Plugin path: ${env.PLUGIN_PATH}`)
log.info(`Environment: ${env.FRAMER_ENV}`)
log.info(`API base: ${getURL(env, "creatorsApiBase")}`)
log.info(`API base: ${getURL(env, "apiBase")}`)
log.info(`Dry run: ${String(env.DRY_RUN)}`)

if (!existsSync(env.PLUGIN_PATH)) {
Expand Down
Loading