climaybe 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ Built by [Electric Maybe](https://electricmaybe.com) — a Shopify-focused produ
7
7
  **Commit linting and Cursor bundle (optional in both flows):**
8
8
 
9
9
  - **Conventional commit linting:** During `climaybe theme init` or `climaybe app init`, you can install [commitlint](https://commitlint.js.org/) and [Husky](https://typicode.github.io/husky) for [Conventional Commits](https://www.conventionalcommits.org/).
10
- - **Cursor rules + skills:** Opt in to Electric Maybe’s bundled [Cursor](https://cursor.com/) rules and skills under `.cursor/rules/` and `.cursor/skills/` (themes, JS, a11y, commits, changelog, Linear, etc.).
10
+ - **Cursor bundle (rules + skills + subagents):** Opt in to Electric Maybe’s bundled [Cursor](https://cursor.com/) files under `.cursor/rules/`, `.cursor/skills/`, and `.cursor/agents/` (themes, JS, a11y, commits, changelog, Linear, **theme-translator** for locale sync, etc.).
11
11
 
12
12
  ## Command layout (Shopify CLI–style)
13
13
 
@@ -48,7 +48,7 @@ npm install -D climaybe
48
48
  npx climaybe app init
49
49
  ```
50
50
 
51
- Installs optional commitlint/Husky and Cursor rules/skills only. Use [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) for app development and deployment.
51
+ Installs optional commitlint/Husky and the Cursor bundle (rules, skills, agents). Use [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) for app development and deployment.
52
52
 
53
53
  ## Commands
54
54
 
@@ -62,12 +62,12 @@ Interactive setup that configures your repo for CI/CD.
62
62
  4. Asks whether to enable optional **preview + cleanup** workflows (default: yes)
63
63
  5. Asks whether to enable optional **build + Lighthouse** workflows (default: yes)
64
64
  6. Asks whether to enable **commitlint + Husky** (enforce [conventional commits](https://www.conventionalcommits.org/) on `git commit`)
65
- 7. Asks whether to install **Cursor rules + skills** (`.cursor/rules/`, `.cursor/skills/`) — Electric Maybe conventions for themes and AI workflows
65
+ 7. Asks whether to install the **Cursor bundle** (`.cursor/rules/`, `.cursor/skills/`, `.cursor/agents/`) — Electric Maybe conventions for themes and AI workflows
66
66
  8. Based on store count, sets up **single-store** or **multi-store** mode
67
67
  9. Writes `package.json` config
68
68
  10. Scaffolds GitHub Actions workflows
69
69
  11. Creates git branches and store directories (multi-store)
70
- 12. Optionally installs commitlint, Husky, and the Cursor bundle (rules + skills)
70
+ 12. Optionally installs commitlint, Husky, and the Cursor bundle (rules, skills, agents)
71
71
 
72
72
  ### `climaybe app init`
73
73
 
@@ -135,13 +135,13 @@ npx climaybe setup-commitlint
135
135
 
136
136
  ### `climaybe add-cursor`
137
137
 
138
- Install Electric Maybe **Cursor rules and skills** into `.cursor/rules/` and `.cursor/skills/`. Use this if you skipped the bundle at init or want to refresh from the version of climaybe you have installed.
138
+ Install Electric Maybe **Cursor rules, skills, and subagents** into `.cursor/rules/`, `.cursor/skills/`, and `.cursor/agents/` (including **theme-translator** for `theme/locales/`). Use this if you skipped the bundle at init or want to refresh from the version of climaybe you have installed.
139
139
 
140
140
  ```bash
141
141
  npx climaybe add-cursor
142
142
  ```
143
143
 
144
- The previous command name `add-cursor-skill` still works as an alias. Re-running replaces the bundled rule and skill files with the copies shipped by your installed climaybe version (same idea as `update-workflows`).
144
+ The previous command name `add-cursor-skill` still works as an alias. Re-running replaces the bundled rules, skills, and subagent files with the copies shipped by your installed climaybe version (same idea as `update-workflows`).
145
145
 
146
146
  ## Configuration
147
147
 
@@ -241,7 +241,6 @@ When enabled, `init` validates required theme files and exits with an error if a
241
241
 
242
242
  `init` auto-creates:
243
243
  - `assets/`
244
- - `release-notes.md` (starter template)
245
244
 
246
245
  `climaybe` auto-installs the shared build script at `.climaybe/build-scripts.js` during workflow scaffolding.
247
246
 
@@ -249,7 +248,7 @@ When enabled, `init` validates required theme files and exits with an error if a
249
248
  |----------|---------|-------------|
250
249
  | `build-pipeline.yml` | Push to any branch | Runs reusable build and Lighthouse checks (when required secrets exist) |
251
250
  | `reusable-build.yml` | workflow_call | Runs Node build + Tailwind compile, then commits compiled assets when changed |
252
- | `create-release.yml` | Push tag `v*` | Builds release archive and creates GitHub Release using `release-notes.md` |
251
+ | `create-release.yml` | Push tag `v*`, or **workflow_run** after Post-Merge Tag / Nightly Hotfix Tag succeed on `main` | Builds release archive and creates GitHub Release notes from commits since the previous tag. If commit subjects are low-signal and `GEMINI_API_KEY` exists, it uses Gemini to generate cleaner merchant-facing notes. Also covers tags created by workflows with `GITHUB_TOKEN`, which may not trigger tag-push workflows. |
253
252
 
254
253
  ### Optional theme dev kit package
255
254
 
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 2.3.0
1
+ 2.4.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Shopify CLI by Electric Maybe for theme CI/CD workflows, branch orchestration, app setup, and dev tooling",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,17 +3,19 @@ import { writeConfig } from '../lib/config.js';
3
3
  import { scaffoldCursorBundle } from '../lib/cursor-bundle.js';
4
4
 
5
5
  /**
6
- * Install Electric Maybe Cursor rules and skills (.cursor/rules, .cursor/skills).
6
+ * Install Electric Maybe Cursor bundle (.cursor/rules, .cursor/skills, .cursor/agents).
7
7
  * Can be run standalone or after init if Cursor bundle was skipped.
8
8
  */
9
9
  export async function addCursorSkillCommand() {
10
- console.log(pc.bold('\n climaybe — Add Cursor rules + skills\n'));
10
+ console.log(pc.bold('\n climaybe — Add Cursor bundle\n'));
11
11
 
12
12
  writeConfig({ cursor_skills: true });
13
13
 
14
14
  const ok = scaffoldCursorBundle();
15
15
  if (ok) {
16
- console.log(pc.green(' Installed .cursor/rules and .cursor/skills from climaybe bundle.'));
16
+ console.log(
17
+ pc.green(' Installed .cursor/rules, .cursor/skills, and .cursor/agents from climaybe bundle.'),
18
+ );
17
19
  console.log(pc.dim(' See .cursor/rules/00-rule-index.mdc for which rules apply when.\n'));
18
20
  } else {
19
21
  console.log(pc.red(' Cursor bundle not found in this climaybe install.'));
@@ -42,7 +42,9 @@ async function runAppInitFlow() {
42
42
  if (enableCursorSkills) {
43
43
  const cursorOk = scaffoldCursorBundle();
44
44
  if (cursorOk) {
45
- console.log(pc.green(' Electric Maybe Cursor rules + skills → .cursor/rules, .cursor/skills'));
45
+ console.log(
46
+ pc.green(' Electric Maybe Cursor bundle → .cursor/rules, .cursor/skills, .cursor/agents'),
47
+ );
46
48
  } else {
47
49
  console.log(pc.yellow(' Cursor bundle not found in package (skipped).'));
48
50
  }
@@ -50,7 +52,7 @@ async function runAppInitFlow() {
50
52
 
51
53
  console.log(pc.bold(pc.green('\n App setup complete!\n')));
52
54
  console.log(pc.dim(' commitlint + Husky: ' + (enableCommitlint ? 'enabled' : 'disabled')));
53
- console.log(pc.dim(' Cursor rules + skills: ' + (enableCursorSkills ? 'installed' : 'skipped')));
55
+ console.log(pc.dim(' Cursor bundle: ' + (enableCursorSkills ? 'installed' : 'skipped')));
54
56
  console.log(pc.dim('\n Next steps:'));
55
57
  console.log(pc.dim(' Use Shopify CLI (`shopify app dev`, etc.) for app development.'));
56
58
  console.log(pc.dim(' Theme CI/CD workflows are not installed; add your own deployment as needed.\n'));
@@ -112,7 +112,7 @@ async function runInitFlow() {
112
112
  includeBuild: enableBuildWorkflows,
113
113
  });
114
114
 
115
- // 7. Optional commitlint + Husky and Cursor rules + skills bundle
115
+ // 7. Optional commitlint + Husky and Cursor bundle (rules, skills, agents)
116
116
  if (enableCommitlint) {
117
117
  console.log(pc.dim(' Setting up commitlint + Husky...'));
118
118
  if (scaffoldCommitlint()) {
@@ -124,7 +124,9 @@ async function runInitFlow() {
124
124
  if (enableCursorSkills) {
125
125
  const cursorOk = scaffoldCursorBundle();
126
126
  if (cursorOk) {
127
- console.log(pc.green(' Electric Maybe Cursor rules + skills → .cursor/rules, .cursor/skills'));
127
+ console.log(
128
+ pc.green(' Electric Maybe Cursor bundle → .cursor/rules, .cursor/skills, .cursor/agents'),
129
+ );
128
130
  } else {
129
131
  console.log(pc.yellow(' Cursor bundle not found in package (skipped).'));
130
132
  }
@@ -163,7 +165,7 @@ async function runInitFlow() {
163
165
  console.log(pc.dim(` VS Code tasks: ${enableVSCodeTasks ? 'enabled' : 'disabled'}`));
164
166
  }
165
167
  console.log(pc.dim(` commitlint + Husky: ${enableCommitlint ? 'enabled' : 'disabled'}`));
166
- console.log(pc.dim(` Cursor rules + skills: ${enableCursorSkills ? 'installed' : 'skipped'}`));
168
+ console.log(pc.dim(` Cursor bundle: ${enableCursorSkills ? 'installed' : 'skipped'}`));
167
169
 
168
170
  const suggestedTag = getSuggestedTagForRelease();
169
171
  const tagLabel = suggestedTag === 'v1.0.0' ? 'Tag your first release' : 'Tag your next release';
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: theme-translator
3
+ description: >-
4
+ Use when changes are detected in theme/locales/en.default.schema.json or
5
+ theme/locales/en.default.json. Generates translations for other locale files
6
+ from new, modified, or removed English entries—including both
7
+ [country-code].json and [country-code].schema.json. Use proactively after
8
+ English locale edits or when reviewing locale diffs.
9
+ model: claude-4-sonnet
10
+ ---
11
+
12
+ You are an expert multilingual translator specializing in e-commerce and Shopify theme localization. Your primary responsibility is to maintain translation consistency across all locale files in a Shopify theme project.
13
+
14
+ ## Core tasks
15
+
16
+ 1. **Monitor changes** — Analyze modifications in `theme/locales/en.default.schema.json` and `theme/locales/en.default.json`: new keys, changed values, and deleted entries.
17
+
18
+ 2. **Generate accurate translations** for every other locale file. You must:
19
+ - Preserve the exact JSON structure and key hierarchy
20
+ - Keep terminology consistent within each language
21
+ - Prefer e-commerce and online-shopping context for ambiguous terms
22
+ - Reuse phrasing from existing strings in the same locale when it matches the same concept
23
+ - Follow language-specific capitalization, punctuation, and formatting norms
24
+
25
+ 3. **Schema files** — For `.schema.json` files, translate UI labels, descriptions, and info text. Preserve technical identifiers and settings structure; do not translate keys meant to stay stable for Shopify or internal references.
26
+
27
+ 4. **Quality assurance**
28
+ - Cross-check similar keys in the same locale for consistent wording
29
+ - Match appropriate brand tone per market
30
+ - Preserve placeholder syntax exactly (e.g. `{{ variable }}`, Liquid-style tokens) as in the English source unless the locale requires a different order—never drop or corrupt placeholders
31
+ - Prefer concise UI-friendly phrasing (avoid unnecessarily long strings)
32
+
33
+ 5. **Best practices**
34
+ - Use market-appropriate shopping terms (e.g. cart vs basket)
35
+ - Align register (formal/informal) with typical e-commerce expectations for that locale
36
+ - Keep widely recognized English UI terms when locals expect them (e.g. checkout, sale), unless the locale file already established a different convention—then stay consistent with that file
37
+
38
+ ## Workflow
39
+
40
+ 1. List all locale files under `theme/locales/`.
41
+ 2. Diff English defaults (`en.default.json`, `en.default.schema.json`) against each other locale’s matching files.
42
+ 3. For **additions/updates**: add or update translated values; mirror structure exactly.
43
+ 4. For **deletions** in English: remove the same keys from other locales (or align with team policy if documented—default is remove to avoid stale keys).
44
+ 5. Ensure output is **valid JSON** (no trailing commas, correct quoting, stable key order optional but structure must match English).
45
+ 6. End with a **short summary** grouped by locale: added / modified / deleted.
46
+
47
+ ## Invocation examples (for the parent agent)
48
+
49
+ - User added new keys in English → run theme-translator to propagate to all locales.
50
+ - User changed English copy → update the same keys across locales.
51
+ - During review, English locale diffs are present → proactively sync other languages.
52
+
53
+ Never break JSON syntax. When unsure of a term, prefer consistency with existing strings in that locale file over literal word-for-word translation.
@@ -13,7 +13,7 @@ For context on how the project uses locales (e.g. in sections/snippets), the rul
13
13
 
14
14
  1. `.cursor/rules/00-rule-index.mdc` — rule index (schemas.mdc prefers translation keys over schema text; locales live in `locales/`)
15
15
 
16
- No single "locale rules" file is required; this skill focuses on file comparison and reporting.
16
+ No single "locale rules" file is required; this skill focuses on file comparison and reporting. To **generate** translations across locales after English changes, use the **theme-translator** subagent (`/theme-translator`), installed with the Cursor bundle under `.cursor/agents/`.
17
17
 
18
18
  ## Workflow
19
19
 
package/src/index.js CHANGED
@@ -84,7 +84,7 @@ export function createProgram(version = '0.0.0', packageDir = '') {
84
84
  const app = program.command('app').description('Shopify app repo helpers (no theme workflows)');
85
85
  app
86
86
  .command('init')
87
- .description('Set up commitlint, Cursor rules/skills, and project_type: app in package.json')
87
+ .description('Set up commitlint, Cursor bundle (rules/skills/agents), and project_type: app in package.json')
88
88
  .action(appInitCommand);
89
89
 
90
90
  program
@@ -95,7 +95,9 @@ export function createProgram(version = '0.0.0', packageDir = '') {
95
95
  program
96
96
  .command('add-cursor')
97
97
  .alias('add-cursor-skill')
98
- .description('Install Electric Maybe Cursor rules + skills (.cursor/rules, .cursor/skills)')
98
+ .description(
99
+ 'Install Electric Maybe Cursor bundle (.cursor/rules, .cursor/skills, .cursor/agents)',
100
+ )
99
101
  .action(addCursorSkillCommand);
100
102
 
101
103
  return program;
@@ -23,14 +23,6 @@ const REQUIRED_PATHS = [
23
23
 
24
24
  const DEFAULTS = [
25
25
  { path: 'assets', kind: 'dir' },
26
- {
27
- path: 'release-notes.md',
28
- kind: 'file',
29
- content: `# Release Notes
30
-
31
- - describe key changes for this release
32
- `,
33
- },
34
26
  ];
35
27
 
36
28
  function targetScriptPath(cwd = process.cwd()) {
package/src/lib/config.js CHANGED
@@ -143,7 +143,7 @@ export function isCommitlintEnabled(cwd = process.cwd()) {
143
143
  }
144
144
 
145
145
  /**
146
- * Whether bundled Cursor rules + skills were installed (init or add-cursor).
146
+ * Whether bundled Cursor rules, skills, and subagents were installed (init or add-cursor).
147
147
  */
148
148
  export function isCursorSkillsEnabled(cwd = process.cwd()) {
149
149
  const config = readConfig(cwd);
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
 
7
- /** Bundled Electric Maybe Cursor rules + skills (shipped under src/cursor/). */
7
+ /** Bundled Electric Maybe Cursor rules, skills, and subagents (shipped under src/cursor/). */
8
8
  const BUNDLE_ROOT = join(__dirname, '..', 'cursor');
9
9
 
10
10
  const SKIP_NAMES = new Set(['.DS_Store']);
@@ -30,18 +30,20 @@ function copyTree(src, dest) {
30
30
  }
31
31
 
32
32
  /**
33
- * Install bundled `.cursor/rules` and `.cursor/skills` into the target repo.
33
+ * Install bundled `.cursor/rules`, `.cursor/skills`, and `.cursor/agents` into the target repo.
34
34
  * @param {string} [cwd] - Working directory (default process.cwd())
35
35
  * @returns {boolean} - false if bundle source is missing (broken install)
36
36
  */
37
37
  export function scaffoldCursorBundle(cwd = process.cwd()) {
38
38
  const rulesSrc = join(BUNDLE_ROOT, 'rules');
39
39
  const skillsSrc = join(BUNDLE_ROOT, 'skills');
40
- if (!existsSync(rulesSrc) || !existsSync(skillsSrc)) {
40
+ const agentsSrc = join(BUNDLE_ROOT, 'agents');
41
+ if (!existsSync(rulesSrc) || !existsSync(skillsSrc) || !existsSync(agentsSrc)) {
41
42
  return false;
42
43
  }
43
44
  const cursorRoot = join(cwd, '.cursor');
44
45
  copyTree(rulesSrc, join(cursorRoot, 'rules'));
45
46
  copyTree(skillsSrc, join(cursorRoot, 'skills'));
47
+ copyTree(agentsSrc, join(cursorRoot, 'agents'));
46
48
  return true;
47
49
  }
@@ -192,14 +192,14 @@ export async function promptCommitlint() {
192
192
  }
193
193
 
194
194
  /**
195
- * Ask whether to install bundled Cursor rules + skills (.cursor/rules, .cursor/skills).
195
+ * Ask whether to install bundled Cursor rules, skills, and subagents (.cursor/rules, .cursor/skills, .cursor/agents).
196
196
  */
197
197
  export async function promptCursorSkills() {
198
198
  const { enableCursorSkills } = await prompts({
199
199
  type: 'confirm',
200
200
  name: 'enableCursorSkills',
201
201
  message:
202
- 'Install Electric Maybe Cursor rules + skills? (Liquid/JS/a11y conventions + AI skills in .cursor/)',
202
+ 'Install Electric Maybe Cursor bundle? (rules, skills, subagents e.g. theme-translator in .cursor/)',
203
203
  initial: true,
204
204
  });
205
205
 
@@ -53,6 +53,9 @@ yarn.lock
53
53
  startServerCommand: 'shopify theme serve',
54
54
  startServerReadyPattern: 'Preview your theme',
55
55
  startServerReadyTimeout: 60000,
56
+ settings: {
57
+ chromeFlags: '--no-sandbox --disable-dev-shm-usage',
58
+ },
56
59
  },
57
60
  assert: {
58
61
  assertions: {
@@ -4,21 +4,69 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - "v*"
7
+ workflow_run:
8
+ workflows: ['Post-Merge Tag', 'Nightly Hotfix Tag']
9
+ types: [completed]
10
+ branches: [main]
11
+
12
+ permissions:
13
+ contents: write
7
14
 
8
15
  jobs:
9
16
  build:
17
+ if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
10
18
  runs-on: ubuntu-latest
11
19
  steps:
12
20
  - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+
24
+ - name: Resolve release tag
25
+ id: tag
26
+ run: |
27
+ if [ "${{ github.event_name }}" = "push" ]; then
28
+ TAG_NAME="${GITHUB_REF#refs/tags/}"
29
+ else
30
+ git fetch --tags origin
31
+ TAG_NAME=$(git tag --merged HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
32
+ if [ -z "$TAG_NAME" ]; then
33
+ TAG_NAME=$(git tag --merged HEAD | grep -E '^v[0-9]+\.[0-9]+$' | sort -V | tail -1)
34
+ fi
35
+ fi
36
+
37
+ if [ -z "$TAG_NAME" ]; then
38
+ echo "No release tag found, skipping."
39
+ echo "tag_name=" >> $GITHUB_OUTPUT
40
+ exit 0
41
+ fi
42
+
43
+ echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
44
+ echo "Using tag: $TAG_NAME"
45
+
46
+ - name: Check if release already exists
47
+ id: release_check
48
+ env:
49
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50
+ run: |
51
+ TAG_NAME="${{ steps.tag.outputs.tag_name }}"
52
+ if [ -z "$TAG_NAME" ]; then
53
+ echo "release_exists=true" >> $GITHUB_OUTPUT
54
+ exit 0
55
+ fi
56
+
57
+ if gh release view "$TAG_NAME" >/dev/null 2>&1; then
58
+ echo "Release for $TAG_NAME already exists, skipping."
59
+ echo "release_exists=true" >> $GITHUB_OUTPUT
60
+ else
61
+ echo "release_exists=false" >> $GITHUB_OUTPUT
62
+ fi
13
63
 
14
64
  - name: Create release archive
65
+ if: steps.release_check.outputs.release_exists != 'true'
15
66
  run: |
16
67
  mkdir release-temp
17
68
  mkdir -p .sys/release-notes
18
69
 
19
- TAG_NAME=${GITHUB_REF#refs/tags/}
20
- cp release-notes.md ".sys/release-notes/release-note-${TAG_NAME}.md"
21
-
22
70
  for dir in assets layout locales sections snippets config; do
23
71
  mkdir -p "release-temp/$dir"
24
72
  find "$dir" -type f ! -name "*.md" -exec cp --parents {} release-temp/ \;
@@ -43,10 +91,113 @@ jobs:
43
91
  cd ..
44
92
  rm -rf release-temp
45
93
 
94
+ - name: Generate release notes from commits
95
+ if: steps.release_check.outputs.release_exists != 'true'
96
+ id: notes
97
+ env:
98
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
99
+ run: |
100
+ TAG_NAME="${{ steps.tag.outputs.tag_name }}"
101
+ NOTES_FILE=".sys/release-notes/release-note-${TAG_NAME}.md"
102
+ mkdir -p .sys/release-notes
103
+
104
+ git fetch --tags origin
105
+
106
+ PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -Fxv "$TAG_NAME" | head -1 || true)
107
+ if [ -z "$PREV_TAG" ]; then
108
+ PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+$' | grep -Fxv "$TAG_NAME" | head -1 || true)
109
+ fi
110
+
111
+ if [ -n "$PREV_TAG" ]; then
112
+ RANGE="${PREV_TAG}..${TAG_NAME}"
113
+ else
114
+ RANGE="$TAG_NAME"
115
+ fi
116
+
117
+ COMMITS=$(git log --pretty=format:"%s" "$RANGE" 2>/dev/null || true)
118
+ if [ -z "$COMMITS" ]; then
119
+ COMMITS=$(git log --pretty=format:"%s" -n 30 "$TAG_NAME" 2>/dev/null || true)
120
+ fi
121
+
122
+ # Score how many commit subjects follow conventional-commit style.
123
+ TOTAL_COUNT=$(printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
124
+ CONVENTIONAL_COUNT=$(printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | grep -Eic '^(feat|fix|refactor|perf|docs|style|test|build|ci|chore|revert)(\([^)]+\))?!?: ')
125
+ if [ -z "$TOTAL_COUNT" ] || [ "$TOTAL_COUNT" -eq 0 ]; then
126
+ TOTAL_COUNT=1
127
+ fi
128
+ QUALITY=$((CONVENTIONAL_COUNT * 100 / TOTAL_COUNT))
129
+
130
+ {
131
+ echo "# Release Notes"
132
+ echo
133
+ echo "## ${TAG_NAME}"
134
+ if [ -n "$PREV_TAG" ]; then
135
+ echo
136
+ echo "_Changes since ${PREV_TAG}_"
137
+ fi
138
+ echo
139
+ } > "$NOTES_FILE"
140
+
141
+ # If commit quality is low and Gemini is available, ask AI to summarize merchant-facing notes.
142
+ if [ "$QUALITY" -lt 50 ] && [ -n "$GEMINI_API_KEY" ]; then
143
+ printf '%s\n' "$COMMITS" > .sys/release-notes/commits-for-ai.txt
144
+
145
+ PROMPT=$(cat <<'PROMPT_EOF'
146
+ You generate release notes for a Shopify theme.
147
+ Given commit subjects, write concise markdown with these sections when relevant:
148
+ - Features
149
+ - Fixes
150
+ - Improvements
151
+ - Chore
152
+
153
+ Rules:
154
+ - Keep notes merchant-friendly and specific.
155
+ - Do not include commit hashes.
156
+ - Do not invent changes.
157
+ - If a section has no items, omit it.
158
+ PROMPT_EOF
159
+ )
160
+
161
+ PAYLOAD=$(jq -n \
162
+ --arg prompt "$PROMPT" \
163
+ --rawfile commits .sys/release-notes/commits-for-ai.txt \
164
+ '{
165
+ "contents": [{
166
+ "parts": [{"text": ($prompt + "\n\nCommits:\n" + $commits)}]
167
+ }],
168
+ "generationConfig": {
169
+ "temperature": 0.2,
170
+ "maxOutputTokens": 1024
171
+ }
172
+ }')
173
+
174
+ RESPONSE=$(curl -s -X POST \
175
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}" \
176
+ -H "Content-Type: application/json" \
177
+ -d "$PAYLOAD")
178
+ AI_NOTES=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].text // empty' 2>/dev/null || true)
179
+
180
+ if [ -n "$AI_NOTES" ] && [ "$AI_NOTES" != "null" ]; then
181
+ printf '%s\n' "$AI_NOTES" >> "$NOTES_FILE"
182
+ else
183
+ echo "## Changes" >> "$NOTES_FILE"
184
+ echo >> "$NOTES_FILE"
185
+ printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | sed 's/^/- /' >> "$NOTES_FILE"
186
+ fi
187
+ else
188
+ echo "## Changes" >> "$NOTES_FILE"
189
+ echo >> "$NOTES_FILE"
190
+ printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | sed 's/^/- /' >> "$NOTES_FILE"
191
+ fi
192
+
193
+ echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT
194
+
46
195
  - name: Create Release
196
+ if: steps.release_check.outputs.release_exists != 'true'
47
197
  uses: softprops/action-gh-release@v1
48
198
  with:
199
+ tag_name: ${{ steps.tag.outputs.tag_name }}
49
200
  files: release.zip
50
- body_path: release-notes.md
201
+ body_path: ${{ steps.notes.outputs.notes_file }}
51
202
  env:
52
203
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}