diff --git a/.github/workflows/submit-on-merge.yml b/.github/workflows/submit-on-merge.yml index 77ac92bd4..6bada1c50 100644 --- a/.github/workflows/submit-on-merge.yml +++ b/.github/workflows/submit-on-merge.yml @@ -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 }} diff --git a/.github/workflows/submit-plugin.yml b/.github/workflows/submit-plugin.yml index c6236c72e..cf4deb7f8 100644 --- a/.github/workflows/submit-plugin.yml +++ b/.github/workflows/submit-plugin.yml @@ -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 @@ -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 }} diff --git a/scripts/lib/env.ts b/scripts/lib/env.ts index 3bafbfe8e..524b004a0 100644 --- a/scripts/lib/env.ts +++ b/scripts/lib/env.ts @@ -27,7 +27,6 @@ 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 @@ -35,7 +34,6 @@ export type Environment = v.InferOutput export interface EnvironmentUrls { apiBase: string - creatorsApiBase: string framerAppUrl: string marketplaceBaseUrl: string } @@ -43,13 +41,11 @@ export interface EnvironmentUrls { export const ENVIRONMENT_URLS: Record = { 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", }, diff --git a/scripts/lib/framer-api.ts b/scripts/lib/framer-api.ts index e512881b0..5039fda6f 100644 --- a/scripts/lib/framer-api.ts +++ b/scripts/lib/framer-api.ts @@ -51,6 +51,11 @@ const SubmissionResponseSchema = v.object({ }) export type SubmissionResponse = v.InferOutput +/** 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(), @@ -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 { - 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 { + 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 { + 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 } diff --git a/scripts/submit-on-merge.ts b/scripts/submit-on-merge.ts index 1c89cf7d6..e29c19c8f 100644 --- a/scripts/submit-on-merge.ts +++ b/scripts/submit-on-merge.ts @@ -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" diff --git a/scripts/submit-plugin.ts b/scripts/submit-plugin.ts index 4ea91142d..6ba7d6c70 100644 --- a/scripts/submit-plugin.ts +++ b/scripts/submit-plugin.ts @@ -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) @@ -50,7 +49,7 @@ async function main(): Promise { 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)) {