From 1f747442199f9957ec55423d7410b5e1b3378f9d Mon Sep 17 00:00:00 2001 From: Niek Date: Thu, 12 Feb 2026 15:19:32 +0100 Subject: [PATCH 1/8] Make submit-plugin.yml reusable from other repos - Dual checkout: caller repo as source/, plugins repo as tooling/ - Add pr_body input as alternative to changelog (extracts ### Changelog section) - Make CHANGELOG optional in submit-plugin.ts when PR_BODY is provided - Pass changelog as explicit parameter through function chain NOTE: tooling repo temporarily points to niekert/plugins for testing Co-Authored-By: Claude Opus 4.6 --- .github/workflows/submit-plugin.yml | 50 ++++++++++++++++++++--------- scripts/lib/env.ts | 3 +- scripts/lib/framer-api.ts | 9 ++++-- scripts/lib/git.ts | 5 ++- scripts/lib/slack.ts | 5 +-- scripts/submit-plugin.ts | 25 +++++++++++---- 6 files changed, 68 insertions(+), 29 deletions(-) diff --git a/.github/workflows/submit-plugin.yml b/.github/workflows/submit-plugin.yml index cec0ab0de..fb84bc23e 100644 --- a/.github/workflows/submit-plugin.yml +++ b/.github/workflows/submit-plugin.yml @@ -26,16 +26,20 @@ on: default: false type: boolean - # Reusable workflow - can be called from other repos (e.g., framer/workshop) + # Reusable workflow - can be called from other repos workflow_call: inputs: plugin_path: - description: 'Plugin directory (e.g., plugins/csv-import)' + description: 'Plugin directory relative to repo root (e.g., "plugins/csv-import" or ".")' required: true type: string changelog: - description: 'Changelog for this release' - required: true + description: 'Changelog text (mutually exclusive with pr_body)' + required: false + type: string + pr_body: + description: 'Full PR body — changelog will be extracted (mutually exclusive with changelog)' + required: false type: string environment: description: 'Environment (development/production)' @@ -71,26 +75,38 @@ jobs: environment: ${{ inputs.environment }} steps: - - name: Checkout repository + # Checkout the caller's repo (plugin source + git history for tags) + - name: Checkout plugin source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: source + + # Checkout the plugins repo (scripts + tooling). + # For workflow_dispatch this is redundant (same repo), but keeps the flow uniform. + - name: Checkout tooling uses: actions/checkout@v4 with: - fetch-depth: 0 # Full history for git tags and diff + repository: niekert/plugins + path: tooling - name: Configure git identity + working-directory: source run: | - git config --global user.email "marketplace@framer.team" - git config --global user.name "Framer Marketplace" + git config user.email "marketplace@framer.team" + git config user.name "Framer Marketplace" - name: Validate plugin path + working-directory: source run: | - if [ ! -d "${{ github.workspace }}/${{ inputs.plugin_path }}" ]; then + if [ ! -d "${{ inputs.plugin_path }}" ]; then echo "Error: Plugin path '${{ inputs.plugin_path }}' does not exist" echo "" - echo "Available plugins:" - ls -1 plugins/ + echo "Available contents:" + ls -1 exit 1 fi - if [ ! -f "${{ github.workspace }}/${{ inputs.plugin_path }}/framer.json" ]; then + if [ ! -f "${{ inputs.plugin_path }}/framer.json" ]; then echo "Error: No framer.json found in '${{ inputs.plugin_path }}'" exit 1 fi @@ -99,20 +115,24 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version-file: .tool-versions + node-version-file: tooling/.tool-versions - name: Install dependencies + working-directory: tooling run: yarn install - name: Build framer-plugin-tools + working-directory: tooling run: yarn turbo run build --filter=framer-plugin-tools - name: Submit plugin + working-directory: tooling run: yarn tsx scripts/submit-plugin.ts env: - PLUGIN_PATH: ${{ github.workspace }}/${{ inputs.plugin_path }} - REPO_ROOT: ${{ github.workspace }} + PLUGIN_PATH: ${{ github.workspace }}/source/${{ inputs.plugin_path }} + REPO_ROOT: ${{ github.workspace }}/source 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 }} diff --git a/scripts/lib/env.ts b/scripts/lib/env.ts index e50453f7a..ace276eba 100644 --- a/scripts/lib/env.ts +++ b/scripts/lib/env.ts @@ -9,7 +9,8 @@ export const BooleanEnvSchema = v.pipe( export const EnvSchema = v.object({ PLUGIN_PATH: v.pipe(v.string(), v.minLength(1)), - CHANGELOG: v.pipe(v.string(), v.minLength(1)), + CHANGELOG: v.optional(v.pipe(v.string(), v.minLength(1))), + PR_BODY: v.optional(v.string()), SLACK_WEBHOOK_URL: v.optional(v.string()), SLACK_ERROR_WEBHOOK_URL: v.optional(v.string()), RETOOL_URL: v.optional(v.string()), diff --git a/scripts/lib/framer-api.ts b/scripts/lib/framer-api.ts index e958d1d4c..e512881b0 100644 --- a/scripts/lib/framer-api.ts +++ b/scripts/lib/framer-api.ts @@ -111,7 +111,12 @@ export function loadFramerJsonFile(pluginPath: string): FramerJson { return framerJson } -export async function submitPlugin(zipFilePath: string, plugin: Plugin, env: Environment): Promise { +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") } @@ -125,7 +130,7 @@ export async function submitPlugin(zipFilePath: string, plugin: Plugin, env: Env const formData = new FormData() formData.append("file", blob, "plugin.zip") - formData.append("content", await changelogToHtml(env.CHANGELOG)) + formData.append("content", await changelogToHtml(changelog)) const response = await fetch(url, { method: "POST", diff --git a/scripts/lib/git.ts b/scripts/lib/git.ts index 4b84e02e8..97f979f93 100644 --- a/scripts/lib/git.ts +++ b/scripts/lib/git.ts @@ -1,8 +1,7 @@ import { execFileSync } from "node:child_process" -import type { Environment } from "./env" import { log } from "./logging" -export function createGitTag(pluginName: string, version: number, repoRoot: string, env: Environment): void { +export function createGitTag(pluginName: string, version: number, repoRoot: string, changelog: string): void { const tagName = `${pluginName.toLowerCase().replace(/\s+/g, "-")}-v${version.toString()}` log.info(`Creating git tag: ${tagName}`) @@ -16,7 +15,7 @@ export function createGitTag(pluginName: string, version: number, repoRoot: stri // Tag doesn't exist, that's fine } - execFileSync("git", ["tag", "-a", tagName, "-m", env.CHANGELOG.trim()], { + execFileSync("git", ["tag", "-a", tagName, "-m", changelog.trim()], { cwd: repoRoot, stdio: "inherit", }) diff --git a/scripts/lib/slack.ts b/scripts/lib/slack.ts index b131183e2..c74f24189 100644 --- a/scripts/lib/slack.ts +++ b/scripts/lib/slack.ts @@ -15,14 +15,15 @@ interface SlackWorkflowPayload { export async function sendSlackNotification( framerJson: FramerJson, submissionResult: SubmissionResponse, - env: Environment + env: Environment, + changelog: string ): Promise { const payload: SlackWorkflowPayload = { pluginName: framerJson.name, pluginVersion: submissionResult.version.toString(), marketplacePreviewUrl: `${getURL(env, "marketplaceBaseUrl")}/plugins/${submissionResult.slug}/preview`, pluginReviewUrl: `${getURL(env, "framerAppUrl")}/projects/new?plugin=${submissionResult.internalPluginId}&pluginVersion=${submissionResult.versionId}`, - changelog: env.CHANGELOG, + changelog: changelog, retoolUrl: env.RETOOL_URL, } diff --git a/scripts/submit-plugin.ts b/scripts/submit-plugin.ts index fc336ac37..20cbd3f9d 100644 --- a/scripts/submit-plugin.ts +++ b/scripts/submit-plugin.ts @@ -9,7 +9,8 @@ * * Environment Variables: * PLUGIN_PATH - Path to the plugin directory (required) - * CHANGELOG - Changelog text (required) + * 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) @@ -29,6 +30,7 @@ import type { FramerJson } from "./lib/framer-api" import { fetchMyPlugins, loadFramerJsonFile, submitPlugin } from "./lib/framer-api" import { createGitTag } from "./lib/git" import { log } from "./lib/logging" +import { extractChangelog } from "./lib/parse-pr" import { sendErrorNotification, sendSlackNotification } from "./lib/slack" async function main(): Promise { @@ -52,6 +54,17 @@ async function main(): Promise { throw new Error(`Plugin path does not exist: ${env.PLUGIN_PATH}`) } + // Resolve changelog from CHANGELOG or PR_BODY + log.step("Resolving Changelog") + let changelog = env.CHANGELOG + if (!changelog && env.PR_BODY) { + changelog = extractChangelog(env.PR_BODY) ?? undefined + } + + if (!changelog) { + throw new Error("No changelog provided. Set CHANGELOG or PR_BODY with a ### Changelog section.") + } + log.step("Loading Plugin Info") framerJson = loadFramerJsonFile(env.PLUGIN_PATH) log.info(`Name: ${framerJson.name}`) @@ -77,7 +90,7 @@ async function main(): Promise { log.info(`Found plugin with ID: ${plugin.id}`) log.step("Changelog") - log.info(`Changelog:\n${env.CHANGELOG}`) + log.info(`Changelog:\n${changelog}`) log.step("Building & Packing Plugin") @@ -94,19 +107,19 @@ async function main(): Promise { if (env.DRY_RUN) { log.step("DRY RUN - Skipping Submission") log.info("Plugin is built and packed. Would submit to API in real run.") - log.info(`Would submit with changelog:\n${env.CHANGELOG}`) + log.info(`Would submit with changelog:\n${changelog}`) return } log.step("Submitting to Framer API") - const submissionResult = await submitPlugin(zipFilePath, plugin, env) + const submissionResult = await submitPlugin(zipFilePath, plugin, env, changelog) log.step("Creating Git Tag") - createGitTag(framerJson.name, submissionResult.version, repoRoot, env) + createGitTag(framerJson.name, submissionResult.version, repoRoot, changelog) if (env.SLACK_WEBHOOK_URL) { log.step("Sending Slack Notification") - await sendSlackNotification(framerJson, submissionResult, env) + await sendSlackNotification(framerJson, submissionResult, env, changelog) } console.log("\n" + "=".repeat(60)) From 04c9089916dc7df67b227e28b3a5c436aaed4ecb Mon Sep 17 00:00:00 2001 From: Niek Date: Thu, 12 Feb 2026 16:19:40 +0100 Subject: [PATCH 2/8] Extract check-pr.yml as reusable workflow - New check-pr.yml: reusable workflow for PR validation + auto-labeling - shippy.yml now calls check-pr.yml instead of inlining the logic - validate-pr-body.ts also outputs changelog text (multiline GITHUB_OUTPUT) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr.yml | 68 ++++++++++++++++++++++++++++++++++ .github/workflows/shippy.yml | 54 ++------------------------- scripts/validate-pr-body.ts | 6 ++- 3 files changed, 76 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/check-pr.yml diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml new file mode 100644 index 000000000..12aca0c4d --- /dev/null +++ b/.github/workflows/check-pr.yml @@ -0,0 +1,68 @@ +name: Check PR + +on: + workflow_call: + outputs: + has_changelog: + description: 'Whether the PR body contains a changelog section' + value: ${{ jobs.check.outputs.has_changelog }} + changelog: + description: 'Extracted changelog text from the PR body' + value: ${{ jobs.check.outputs.changelog }} + +jobs: + check: + name: PR Description Check + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false && github.event.pull_request.user.login != 'dependabot[bot]' + outputs: + has_changelog: ${{ steps.validate.outputs.has_changelog }} + changelog: ${{ steps.validate.outputs.changelog }} + + steps: + - name: Check if Submit on merge label is present + id: check-label + uses: actions/github-script@v7 + with: + script: | + const labels = context.payload.pull_request.labels || [] + const hasSubmitLabel = labels.some(label => label.name === 'Submit on merge') + core.setOutput('require_changelog', hasSubmitLabel ? 'true' : 'false') + + - name: Checkout tooling + uses: actions/checkout@v4 + with: + repository: framer/plugins + sparse-checkout: | + scripts + package.json + yarn.lock + .yarnrc.yml + .yarn + .tool-versions + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .tool-versions + + - name: Validate PR body + id: validate + run: yarn dlx tsx scripts/validate-pr-body.ts + env: + PR_BODY: ${{ github.event.pull_request.body }} + REQUIRE_CHANGELOG: ${{ steps.check-label.outputs.require_changelog }} + + - name: Add Submit on merge label + if: >- + steps.validate.outputs.has_changelog == 'true' && + (github.event.action == 'opened' || github.event.action == 'ready_for_review') + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['Submit on merge'] + }) diff --git a/.github/workflows/shippy.yml b/.github/workflows/shippy.yml index c92a52a80..c5f12f724 100644 --- a/.github/workflows/shippy.yml +++ b/.github/workflows/shippy.yml @@ -11,63 +11,15 @@ on: - unlabeled workflow_dispatch: # NOTE: To prevent GitHub from adding PRs to the merge queue before check is done, - # make sure that there is a ruleset that requires the “Shippy“ check to pass. + # make sure that there is a ruleset that requires the "Shippy" check to pass. merge_group: types: - checks_requested - + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check-pr-description: - name: PR Description Check - runs-on: ubuntu-latest - if: github.event.pull_request.draft == false && github.event.pull_request.user.login != 'dependabot[bot]' - steps: - - name: Check if Auto submit to Marketplace on merge label is present - id: check-label - uses: actions/github-script@v7 - with: - script: | - const labels = context.payload.pull_request.labels || [] - const hasSubmitLabel = labels.some(label => label.name === 'Auto submit to Marketplace on merge') - core.setOutput('require_changelog', hasSubmitLabel ? 'true' : 'false') - - - name: Checkout repository - uses: actions/checkout@v4 - with: - sparse-checkout: | - scripts - package.json - yarn.lock - .yarnrc.yml - .yarn - .tool-versions - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: .tool-versions - - - name: Validate PR body - id: validate - run: npx tsx@4.21.0 scripts/validate-pr-body.ts - env: - PR_BODY: ${{ github.event.pull_request.body }} - REQUIRE_CHANGELOG: ${{ steps.check-label.outputs.require_changelog }} - - - name: Add Auto submit to Marketplace on merge label - if: >- - steps.validate.outputs.has_changelog == 'true' && - (github.event.action == 'opened' || github.event.action == 'ready_for_review') - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: ['Auto submit to Marketplace on merge'] - }) + uses: ./.github/workflows/check-pr.yml diff --git a/scripts/validate-pr-body.ts b/scripts/validate-pr-body.ts index 305209d10..567dd3207 100644 --- a/scripts/validate-pr-body.ts +++ b/scripts/validate-pr-body.ts @@ -28,10 +28,14 @@ if (!prBody) { const changelog = extractChangelog(prBody) -// Write GitHub Actions output (no-op outside CI) +// Write GitHub Actions outputs (no-op outside CI) const outputFile = process.env.GITHUB_OUTPUT if (outputFile) { appendFileSync(outputFile, `has_changelog=${changelog ? "true" : "false"}\n`) + if (changelog) { + // Multiline output requires delimiter syntax + appendFileSync(outputFile, `changelog< Date: Thu, 12 Feb 2026 16:26:59 +0100 Subject: [PATCH 3/8] Fix check-pr.yml: use yarn install + working-directory instead of yarn dlx yarn dlx tsx can't resolve local imports (./lib/parse-pr) because it runs from a temp directory. Use proper install with working-directory instead. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 12aca0c4d..aa3392fa3 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -33,6 +33,7 @@ jobs: uses: actions/checkout@v4 with: repository: framer/plugins + path: tooling sparse-checkout: | scripts package.json @@ -44,11 +45,16 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version-file: .tool-versions + node-version-file: tooling/.tool-versions + + - name: Install dependencies + working-directory: tooling + run: yarn install - name: Validate PR body id: validate - run: yarn dlx tsx scripts/validate-pr-body.ts + working-directory: tooling + run: yarn tsx scripts/validate-pr-body.ts env: PR_BODY: ${{ github.event.pull_request.body }} REQUIRE_CHANGELOG: ${{ steps.check-label.outputs.require_changelog }} From a27d3a8c273c4b8f6697ff3a2e135ccaa0abb2a4 Mon Sep 17 00:00:00 2001 From: Niek Date: Thu, 12 Feb 2026 16:27:59 +0100 Subject: [PATCH 4/8] Point check-pr.yml at niekert/plugins fork for testing Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index aa3392fa3..b29777536 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -32,7 +32,8 @@ jobs: - name: Checkout tooling uses: actions/checkout@v4 with: - repository: framer/plugins + repository: niekert/plugins + ref: reusable-submit-workflow path: tooling sparse-checkout: | scripts From c99601e378b396ef20a83bd7c95b208faf995d25 Mon Sep 17 00:00:00 2001 From: Niek Date: Thu, 12 Feb 2026 16:36:39 +0100 Subject: [PATCH 5/8] Simplify check-pr.yml: use npx tsx, no yarn install needed validate-pr-body.ts only imports node builtins + local files, so we don't need yarn install at all. npx tsx handles it. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr.yml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index b29777536..bfd436e00 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -35,27 +35,17 @@ jobs: repository: niekert/plugins ref: reusable-submit-workflow path: tooling - sparse-checkout: | - scripts - package.json - yarn.lock - .yarnrc.yml - .yarn - .tool-versions + sparse-checkout: scripts - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version-file: tooling/.tool-versions - - - name: Install dependencies - working-directory: tooling - run: yarn install + node-version: 22 - name: Validate PR body id: validate working-directory: tooling - run: yarn tsx scripts/validate-pr-body.ts + run: npx tsx scripts/validate-pr-body.ts env: PR_BODY: ${{ github.event.pull_request.body }} REQUIRE_CHANGELOG: ${{ steps.check-label.outputs.require_changelog }} From 35a6de27cebe4d4820fdbd955e7c90accf727da6 Mon Sep 17 00:00:00 2001 From: Niek Date: Thu, 12 Feb 2026 16:43:41 +0100 Subject: [PATCH 6/8] Pin tsx version in check-pr.yml to avoid supply chain risk Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index bfd436e00..d38b0d526 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -45,7 +45,7 @@ jobs: - name: Validate PR body id: validate working-directory: tooling - run: npx tsx scripts/validate-pr-body.ts + run: npx tsx@4.21.0 scripts/validate-pr-body.ts env: PR_BODY: ${{ github.event.pull_request.body }} REQUIRE_CHANGELOG: ${{ steps.check-label.outputs.require_changelog }} From 1ad3d5bfeb2d672b7d1e60848b30838f5ba70208 Mon Sep 17 00:00:00 2001 From: Niek Date: Mon, 16 Feb 2026 11:29:21 +0100 Subject: [PATCH 7/8] Point check-pr.yml checkout at default branch instead of feature branch Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index d38b0d526..cf1f81688 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -33,7 +33,6 @@ jobs: uses: actions/checkout@v4 with: repository: niekert/plugins - ref: reusable-submit-workflow path: tooling sparse-checkout: scripts From 179747ebea3fb926e100688cc1667b2ebac37f47 Mon Sep 17 00:00:00 2001 From: Niek Date: Mon, 16 Feb 2026 11:31:08 +0100 Subject: [PATCH 8/8] change --- plugins/csv-import/foo | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/csv-import/foo diff --git a/plugins/csv-import/foo b/plugins/csv-import/foo new file mode 100644 index 000000000..e69de29bb