From d480048a5d569327d7cbd5ca199c8fc4213f4fbb Mon Sep 17 00:00:00 2001 From: xavi Date: Tue, 1 Sep 2026 11:42:46 +0200 Subject: [PATCH] chore(scripts): Submit plugin releases through the Creators Service Releases went through the marketplace app, which is behind bot protection and started blocking them. The action now uploads the zip to the plugins API itself and records the release with the Creators Service, so the admin secret is no longer needed. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/submit-on-merge.yml | 1 - .github/workflows/submit-plugin.yml | 4 -- scripts/lib/env.ts | 4 -- scripts/lib/framer-api.ts | 80 +++++++++++++++++++++------ scripts/submit-on-merge.ts | 2 +- scripts/submit-plugin.ts | 3 +- 6 files changed, 66 insertions(+), 28 deletions(-) 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)) {