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 +7 -8
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/add-cursor-skill.js +5 -3
- package/src/commands/app-init.js +4 -2
- package/src/commands/init.js +5 -3
- package/src/cursor/agents/theme-translator.md +53 -0
- package/src/cursor/skills/locale-translation-prep/SKILL.md +1 -1
- package/src/index.js +4 -2
- package/src/lib/build-workflows.js +0 -8
- package/src/lib/config.js +1 -1
- package/src/lib/cursor-bundle.js +5 -3
- package/src/lib/prompts.js +2 -2
- package/src/lib/theme-dev-kit.js +3 -0
- package/src/workflows/build/create-release.yml +155 -4
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/)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1
|
+
2.4.0
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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.'));
|
package/src/commands/app-init.js
CHANGED
|
@@ -42,7 +42,9 @@ async function runAppInitFlow() {
|
|
|
42
42
|
if (enableCursorSkills) {
|
|
43
43
|
const cursorOk = scaffoldCursorBundle();
|
|
44
44
|
if (cursorOk) {
|
|
45
|
-
console.log(
|
|
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
|
|
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'));
|
package/src/commands/init.js
CHANGED
|
@@ -112,7 +112,7 @@ async function runInitFlow() {
|
|
|
112
112
|
includeBuild: enableBuildWorkflows,
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
// 7. Optional commitlint + Husky and Cursor rules
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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);
|
package/src/lib/cursor-bundle.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/prompts.js
CHANGED
|
@@ -192,14 +192,14 @@ export async function promptCommitlint() {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* Ask whether to install bundled Cursor rules
|
|
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
|
|
202
|
+
'Install Electric Maybe Cursor bundle? (rules, skills, subagents e.g. theme-translator in .cursor/)',
|
|
203
203
|
initial: true,
|
|
204
204
|
});
|
|
205
205
|
|
package/src/lib/theme-dev-kit.js
CHANGED
|
@@ -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:
|
|
201
|
+
body_path: ${{ steps.notes.outputs.notes_file }}
|
|
51
202
|
env:
|
|
52
203
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|