Skip to content
Closed

Test #557

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
64 changes: 64 additions & 0 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed label breaks auto-submit-on-merge trigger

High Severity

The new check-pr.yml checks for and auto-applies the label 'Submit on merge', but the existing submit-on-merge.yml workflow triggers only when the label 'Auto submit to Marketplace on merge' is present. This label name mismatch means auto-labeled PRs will never trigger the submission workflow on merge, silently breaking the auto-publish pipeline.

Additional Locations (1)

Fix in Cursor Fix in Web

core.setOutput('require_changelog', hasSubmitLabel ? 'true' : 'false')

- name: Checkout tooling
uses: actions/checkout@v4
with:
repository: niekert/plugins
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded personal fork instead of organization repository

High Severity

Both check-pr.yml and submit-plugin.yml check out tooling from niekert/plugins (a personal fork) instead of framer/plugins (the canonical organization repository, as confirmed in packages/plugin-tools/package.json). This means CI workflows will pull and execute scripts from a personal fork, which is both a correctness and supply-chain risk.

Additional Locations (1)

Fix in Cursor Fix in Web

path: tooling
sparse-checkout: scripts

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Validate PR body
id: validate
working-directory: tooling
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 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']
})
54 changes: 3 additions & 51 deletions .github/workflows/shippy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 35 additions & 15 deletions .github/workflows/submit-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
Empty file added plugins/csv-import/foo
Empty file.
3 changes: 2 additions & 1 deletion scripts/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
9 changes: 7 additions & 2 deletions scripts/lib/framer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ export function loadFramerJsonFile(pluginPath: string): FramerJson {
return framerJson
}

export async function submitPlugin(zipFilePath: string, plugin: Plugin, env: Environment): Promise<SubmissionResponse> {
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")
}
Expand All @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions scripts/lib/git.ts
Original file line number Diff line number Diff line change
@@ -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}`)
Expand All @@ -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",
})
Expand Down
5 changes: 3 additions & 2 deletions scripts/lib/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ interface SlackWorkflowPayload {
export async function sendSlackNotification(
framerJson: FramerJson,
submissionResult: SubmissionResponse,
env: Environment
env: Environment,
changelog: string
): Promise<void> {
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,
}

Expand Down
25 changes: 19 additions & 6 deletions scripts/submit-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<void> {
Expand All @@ -52,6 +54,17 @@ async function main(): Promise<void> {
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}`)
Expand All @@ -77,7 +90,7 @@ async function main(): Promise<void> {
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")

Expand All @@ -94,19 +107,19 @@ async function main(): Promise<void> {
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))
Expand Down
6 changes: 5 additions & 1 deletion scripts/validate-pr-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<<CHANGELOG_EOF\n${changelog}\nCHANGELOG_EOF\n`)
}
}

if (requireChangelog && !changelog) {
Expand Down
Loading