Skip to content

Commit 2ec7364

Browse files
annab1cursoragent
andcommitted
feat: Unify product-pulse prompt into a shared template
Moves the ~85% of the prompt that was byte-identical between elementor and elementor-pro (goal, decision criteria, type classification, writing style, output format, examples, edge cases) into prompt-template.md, owned by this action. Callers now only supply a small product-areas-file with the three things that genuinely differ per repo: PRODUCT_NAME, the PRODUCT_AREAS file-path mapping, and the PRODUCT_ENUM list. A new render-prompt.py substitutes these into the shared template before it's handed to Cursor Agent. Replaces the prompt-file/default-product inputs accordingly. Ref: ED-24831 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1370d61 commit 2ec7364

4 files changed

Lines changed: 263 additions & 23 deletions

File tree

actions/product-pulse/README.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ Uses Cursor Agent to decide whether a merged PR is product-facing, and if so, po
44
user-friendly pulse update to Slack. Ported from the `elementor` and `elementor-pro`
55
repos so both can share one implementation.
66

7-
Each consuming repo keeps its own `product-pulse-prompt.md` (product-area mapping
8-
differs per repo) and a thin wrapper workflow that triggers on `pull_request: closed`
9-
and calls this action.
7+
The full prompt (goal, decision criteria, writing style, output format, examples) lives
8+
here in `prompt-template.md` and is shared by every caller. Each consuming repo only
9+
keeps a small `product-areas.md` file with the three things that genuinely differ per
10+
repo — `PRODUCT_NAME`, the `PRODUCT_AREAS` file-path mapping, and the `PRODUCT_ENUM`
11+
list — plus a thin wrapper workflow that triggers on `pull_request: closed` and calls
12+
this action. See `product-areas.example.md` for the expected format.
1013

1114
## Usage
1215

@@ -35,21 +38,35 @@ jobs:
3538
- uses: elementor/elementor-editor-github-actions/actions/product-pulse@main
3639
with:
3740
pr-number: ${{ github.event.pull_request.number }}
38-
prompt-file: .github/product-pulse-prompt.md
39-
default-product: 'Elementor'
41+
product-areas-file: .github/product-pulse-areas.md
4042
cursor-api-key: ${{ secrets.CURSOR_APIKEY }}
4143
slack-token: ${{ secrets.SLACK_TOKEN }}
4244
slack-channel-id: ${{ secrets.SLACK_PULSE_CHANNEL_ID }}
4345
```
4446
47+
`.github/product-pulse-areas.md` in the caller repo:
48+
49+
```
50+
PRODUCT_NAME: Elementor
51+
52+
PRODUCT_AREAS:
53+
- `modules/editor-one/`**"Editor"**
54+
- `modules/ai/`**"Elementor AI"**
55+
- `core/`, `includes/`, and other paths not listed above → **"Elementor"**
56+
57+
PRODUCT_ENUM: "Elementor", "Editor", "Elementor AI"
58+
```
59+
4560
## Inputs
4661
47-
| Input | Required | Default | Description |
48-
| ------------------ | -------- | -------------- | ------------------------------------------------------- |
49-
| `pr-number` | yes | – | Merged PR number to generate the pulse for |
50-
| `prompt-file` | yes | – | Path to the caller repo's product-pulse prompt markdown |
51-
| `default-product` | no | `Elementor` | Fallback product name when the AI omits one |
52-
| `model` | no | `composer-2.5` | Cursor Agent model used for generation |
53-
| `cursor-api-key` | yes | – | Cursor Agent API key |
54-
| `slack-token` | yes | – | Slack bot token with `chat:write` |
55-
| `slack-channel-id` | yes | – | Slack channel ID for pulse notifications |
62+
| Input | Required | Default | Description |
63+
| --------------------- | -------- | -------------- | -------------------------------------------------------------------------- |
64+
| `pr-number` | yes | – | Merged PR number to generate the pulse for |
65+
| `product-areas-file` | yes | – | Path to the caller repo's `PRODUCT_NAME`/`PRODUCT_AREAS`/`PRODUCT_ENUM` file |
66+
| `model` | no | `composer-2.5` | Cursor Agent model used for generation |
67+
| `cursor-api-key` | yes | – | Cursor Agent API key |
68+
| `slack-token` | yes | – | Slack bot token with `chat:write` |
69+
| `slack-channel-id` | yes | – | Slack channel ID for pulse notifications |
70+
71+
The full generic prompt lives in this action's `prompt-template.md` and is rendered
72+
together with the caller's `product-areas-file` before being sent to Cursor Agent.

actions/product-pulse/action.yml

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ inputs:
55
pr-number:
66
description: 'Pull request number to generate the pulse for'
77
required: true
8-
prompt-file:
9-
description: 'Path (relative to the checked-out caller repo) to the product-pulse prompt markdown file'
8+
product-areas-file:
9+
description: 'Path (relative to the checked-out caller repo) to the repo-specific product-areas markdown file (PRODUCT_NAME/PRODUCT_AREAS/PRODUCT_ENUM). Combined with this action''s shared prompt-template.md to build the full prompt.'
1010
required: true
11-
default-product:
12-
description: 'Fallback product name used when the AI response omits one'
13-
required: false
14-
default: 'Elementor'
1511
model:
1612
description: 'Cursor Agent model to use for pulse generation'
1713
required: false
@@ -47,12 +43,23 @@ runs:
4743
curl https://cursor.com/install -fsS | bash
4844
echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"
4945
46+
- name: Render prompt
47+
shell: bash
48+
env:
49+
ACTION_PATH: ${{ github.action_path }}
50+
PRODUCT_AREAS_FILE: ${{ inputs.product-areas-file }}
51+
run: |
52+
python3 "$ACTION_PATH/scripts/render-prompt.py" \
53+
"$ACTION_PATH/prompt-template.md" \
54+
"$PRODUCT_AREAS_FILE" \
55+
/tmp/rendered_prompt.md \
56+
/tmp/product_name.txt
57+
5058
- name: Generate product pulse
5159
id: generate
5260
shell: bash
5361
env:
5462
CURSOR_API_KEY: ${{ inputs.cursor-api-key }}
55-
PROMPT_FILE: ${{ inputs.prompt-file }}
5663
MODEL: ${{ inputs.model }}
5764
run: |
5865
if [ -z "$CURSOR_API_KEY" ]; then
@@ -66,7 +73,7 @@ runs:
6673
DIFF_DATA=$(head -500 /tmp/diff.txt 2>/dev/null | head -c 100000 || echo "No diff")
6774
6875
{
69-
cat "$PROMPT_FILE"
76+
cat /tmp/rendered_prompt.md
7077
printf '\n## PR Information\n'
7178
printf '%s' "$PR_DATA"
7279
printf '\n\n## Diff (first 500 lines, truncated to 100KB)\n```\n'
@@ -93,9 +100,9 @@ runs:
93100
id: update
94101
shell: bash
95102
env:
96-
DEFAULT_PRODUCT: ${{ inputs.default-product }}
97103
ACTION_PATH: ${{ github.action_path }}
98104
run: |
105+
DEFAULT_PRODUCT=$(cat /tmp/product_name.txt)
99106
RAW_OUTPUT=$(cat /tmp/pulse_output.json)
100107
101108
RESULT_CONTENT=$(echo "$RAW_OUTPUT" | jq -r '.result // empty')
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Product Pulse Generator
2+
3+
You are an AI that generates user-friendly product pulse updates for {{PRODUCT_NAME}}, an Elementor WordPress page-building plugin.
4+
5+
## Your Goal
6+
7+
Analyze a merged PR and decide if it contains product-facing changes. If yes, generate a Lovable-style pulse update. If no, skip it.
8+
9+
## Decision Criteria
10+
11+
### SKIP if the PR is:
12+
- Pure refactoring with no user-visible changes
13+
- CI/CD pipeline changes
14+
- Dependency updates (unless it enables new features)
15+
- Changes only to test files, configs, or internal tooling
16+
- Documentation updates
17+
- Has prefix `chore:`, `refactor:`, `test:`, `ci:` with no user impact
18+
- Package-only version bumps in `packages/` with no user-facing behavior change
19+
- License/tier bookkeeping changes with no visible upgrade prompt or feature change
20+
21+
### INCLUDE if the PR is:
22+
- New features users can interact with
23+
- Bug fixes that users would notice
24+
- UX improvements (performance, visual changes, better flows)
25+
- New widgets or editor capabilities
26+
- Changes to the editor, canvas, or frontend rendering
27+
- New integrations (WooCommerce, forms handlers, dynamic tags, etc.)
28+
- Changes to Elementor AI behavior or UI
29+
30+
## Type Classification
31+
32+
Every included PR must also be classified with a `type`:
33+
34+
- `"feature"` — a brand-new capability that didn't exist before
35+
- `"fix"` — resolves a bug or broken behavior users would have noticed
36+
- `"improvement"` — makes an existing feature faster, smoother, or easier to use, without adding new capability
37+
- `"internal"` — a notable change worth logging but with no direct end-user impact (e.g. new admin-only tooling)
38+
39+
## Product Area Detection
40+
41+
Based on which files the PR touches, determine the product area:
42+
43+
{{PRODUCT_AREAS}}
44+
- If the PR touches multiple areas, pick the primary one (where the main feature lives).
45+
- Changes in `packages/` belong to whichever product area consumes them — check the PR context.
46+
47+
## Output Format
48+
49+
Output ONLY valid JSON in this exact format:
50+
51+
```json
52+
{
53+
"skip": false,
54+
"type": "feature",
55+
"product": "Widgets",
56+
"title": "Loop Through WooCommerce Products",
57+
"description": "You can now build dynamic product grids that automatically loop through your WooCommerce catalog. No more manually adding each product one by one."
58+
}
59+
```
60+
61+
Or if skipping:
62+
63+
```json
64+
{
65+
"skip": true,
66+
"reason": "Internal refactoring with no user-facing changes"
67+
}
68+
```
69+
70+
The `product` field must be one of: {{PRODUCT_ENUM}}.
71+
72+
The `type` field must be one of: `"feature"`, `"fix"`, `"improvement"`, `"internal"`.
73+
74+
## Writing Style
75+
76+
Follow Lovable's product update style:
77+
78+
1. **Title**: Short, benefit-focused (3-6 words)
79+
- MUST clearly hint at what the feature DOES, not just what category it's in
80+
- Good: "Drag Widgets Between Columns", "Faster Editor Load Times", "Custom CSS Per Breakpoint"
81+
- Bad: "Smart Widget Management" (too vague - what does it actually DO?)
82+
- Bad: "Add nested tabs widget", "Implement collection loop transformer"
83+
84+
2. **Description**: 1-2 sentences, explain WHAT and WHY it matters
85+
- Focus on user benefits, not implementation
86+
- Use simple, non-technical language
87+
- Avoid jargon like "component", "service", "endpoint", "module"
88+
- Write in present tense ("You can now...")
89+
- Mention what problem was solved (e.g., "Previously X was limited to Y...")
90+
91+
3. **Tone**: Friendly, clear, exciting but not over-hyped
92+
93+
## Examples
94+
95+
### Good Example (Include):
96+
```json
97+
{
98+
"skip": false,
99+
"type": "feature",
100+
"product": "Editor",
101+
"title": "Drag Widgets Between Columns",
102+
"description": "You can now drag widgets directly from one column to another in the editor. No more copy-paste or delete-and-recreate when rearranging your layout."
103+
}
104+
```
105+
106+
### Bad Example (Too Technical):
107+
```json
108+
{
109+
"title": "Nested Carousel Widget Renderer",
110+
"description": "Implemented Nested_Carousel widget with responsive breakpoint support using the atomic widgets schema."
111+
}
112+
```
113+
114+
### Bad Example (Too Vague):
115+
```json
116+
{
117+
"title": "Smart Widget Management",
118+
"description": "Your widgets can now be managed more efficiently in the editor."
119+
}
120+
```
121+
Why it's bad: The title doesn't tell users WHAT the feature does.
122+
123+
### Good Example (Clear Action):
124+
```json
125+
{
126+
"skip": false,
127+
"type": "feature",
128+
"product": "Theme Builder",
129+
"title": "Preview Templates Before Publishing",
130+
"description": "You can now preview how a theme template looks against real content before making it live. Catch layout issues before your visitors do."
131+
}
132+
```
133+
134+
### Good Example (Fix):
135+
```json
136+
{
137+
"skip": false,
138+
"type": "fix",
139+
"product": "Widgets",
140+
"title": "Fixed Broken Icons in Nav Menu",
141+
"description": "Custom icons in the Nav Menu widget no longer disappear when the Inline Font Icons experiment is off."
142+
}
143+
```
144+
145+
### Good Example (Skip):
146+
```json
147+
{
148+
"skip": true,
149+
"reason": "Refactored PHPUnit bootstrap - no user-facing changes"
150+
}
151+
```
152+
153+
## Edge Cases
154+
155+
### Chore-only PR (skip):
156+
A PR titled `chore: update Playwright config` that only touches `.github/workflows/playwright.yml` and `tests/playwright/` → skip. CI and test infrastructure changes are never product-facing.
157+
158+
### Feature PR (include):
159+
A PR that adds a brand-new user-visible widget or capability, touching paths matched in the Product Area Detection list above → include with the corresponding product. New user-visible widgets always qualify.
160+
161+
### Partial `packages/` changes (evaluate carefully):
162+
A PR that only bumps versions or updates a CHANGELOG.md under `packages/` → skip (release housekeeping).
163+
A PR that changes behavior inside a `packages/` source directory with corresponding UI impact → include with the product area that consumes that package. Read the diff and PR body to determine whether the package change reaches users.
164+
165+
## Context You'll Receive
166+
167+
- PR title
168+
- PR description/body
169+
- List of changed files
170+
- Diff (first 500 lines)
171+
172+
Use all context to make an informed decision. If unsure, err on the side of skipping - better to miss a minor update than flood the channel with non-interesting changes.
173+
174+
## Important
175+
176+
- Output ONLY the JSON object, nothing else — no preamble, no explanation, no commentary
177+
- Do NOT wrap it in markdown code blocks
178+
- Valid JSON that can be parsed by `jq`
179+
- Your ENTIRE response must be a single JSON object starting with `{` and ending with `}`
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
import re
3+
import sys
4+
5+
template_path, areas_path, output_prompt_path, output_product_name_path = sys.argv[1:5]
6+
7+
with open(areas_path, encoding="utf-8") as f:
8+
areas_text = f.read()
9+
10+
name_match = re.search(r"^PRODUCT_NAME:\s*(.+)$", areas_text, re.MULTILINE)
11+
enum_match = re.search(r"^PRODUCT_ENUM:\s*(.+)$", areas_text, re.MULTILINE)
12+
areas_match = re.search(r"^PRODUCT_AREAS:\s*\n(.*?)\n+PRODUCT_ENUM:", areas_text, re.DOTALL | re.MULTILINE)
13+
14+
if not (name_match and enum_match and areas_match):
15+
sys.exit(
16+
f"{areas_path} must define PRODUCT_NAME, PRODUCT_AREAS, and PRODUCT_ENUM"
17+
)
18+
19+
product_name = name_match.group(1).strip()
20+
product_areas = areas_match.group(1).strip()
21+
product_enum = enum_match.group(1).strip()
22+
23+
with open(template_path, encoding="utf-8") as f:
24+
rendered = f.read()
25+
26+
rendered = (
27+
rendered
28+
.replace("{{PRODUCT_NAME}}", product_name)
29+
.replace("{{PRODUCT_AREAS}}", product_areas)
30+
.replace("{{PRODUCT_ENUM}}", product_enum)
31+
)
32+
33+
with open(output_prompt_path, "w", encoding="utf-8") as f:
34+
f.write(rendered)
35+
36+
with open(output_product_name_path, "w", encoding="utf-8") as f:
37+
f.write(product_name)

0 commit comments

Comments
 (0)