@vv0rkz/js-template 1.8.3 → 1.9.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 +57 -9
- package/bin/cli.js +13 -0
- package/package.json +1 -1
- package/templates/jst.config.js +47 -4
- package/tools-gh/check-demo-for-release.js +2 -11
- package/tools-gh/config.js +53 -0
- package/tools-gh/post-commit-hook.js +45 -10
- package/tools-gh/release.js +7 -19
- package/tools-gh/report-issue.js +29 -13
- package/tools-gh/setup-branch-protection.js +79 -0
- package/tools-gh/setup-ci.js +63 -0
- package/tools-gh/setup-deps.js +114 -22
- package/tools-gh/version-utils.js +40 -0
package/README.md
CHANGED
|
@@ -52,16 +52,33 @@ export default {
|
|
|
52
52
|
|
|
53
53
|
// Релиз
|
|
54
54
|
release: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
demo: {
|
|
56
|
+
enable: true, // требовать демо перед релизом
|
|
57
|
+
dir: 'docs', // где искать демо-файлы
|
|
58
|
+
formats: ['gif', 'png'], // допустимые форматы
|
|
59
|
+
style: 'click', // 'click' | 'side-by-side'
|
|
60
|
+
},
|
|
58
61
|
},
|
|
59
62
|
|
|
60
63
|
// Автообновление зависимостей
|
|
61
|
-
|
|
64
|
+
// Короткая форма: 'dependabot' | 'renovate' | false
|
|
65
|
+
// Расширенная (только dependabot):
|
|
66
|
+
// { type: 'dependabot', ignoreMajor: true, autoMerge: { patch: true, minor: true } }
|
|
67
|
+
depUpdater: false,
|
|
68
|
+
|
|
69
|
+
// CI workflow (.github/workflows/ci.yml)
|
|
70
|
+
ci: {
|
|
71
|
+
enable: false, // включить генерацию ci.yml
|
|
72
|
+
checks: ['lint', 'test'], // имена job-ов = имена npm-скриптов
|
|
73
|
+
nodeVersion: '20',
|
|
74
|
+
},
|
|
62
75
|
|
|
63
|
-
//
|
|
64
|
-
|
|
76
|
+
// Branch protection (через gh api)
|
|
77
|
+
branchProtection: {
|
|
78
|
+
enable: false,
|
|
79
|
+
requiredChecks: [], // должны совпадать с ci.checks
|
|
80
|
+
enforceAdmins: false,
|
|
81
|
+
},
|
|
65
82
|
}
|
|
66
83
|
```
|
|
67
84
|
|
|
@@ -74,8 +91,10 @@ export default {
|
|
|
74
91
|
| `release.*` | На следующем `jst release` |
|
|
75
92
|
| `labels` | После `jst setup-labels` или `jst apply` |
|
|
76
93
|
| `depUpdater` | После `jst setup-deps` или `jst apply` |
|
|
94
|
+
| `ci.*` | После `jst setup-ci` или `jst apply` |
|
|
95
|
+
| `branchProtection.*` | После `jst setup-branch-protection` (вручную) |
|
|
77
96
|
|
|
78
|
-
Короткий путь — `jst apply` применит labels + deps за раз.
|
|
97
|
+
Короткий путь — `jst apply` применит labels + deps + ci за раз. Branch protection требует admin-прав и запускается отдельно.
|
|
79
98
|
|
|
80
99
|
## Команды
|
|
81
100
|
|
|
@@ -91,9 +110,11 @@ npx jst <команда> # напрямую
|
|
|
91
110
|
|---|---|
|
|
92
111
|
| `init` | Инициализация нового проекта |
|
|
93
112
|
| `upgrade` | Обновить конфиги после `npm update` |
|
|
94
|
-
| `apply` | Применить изменения из `jst.config.js` |
|
|
113
|
+
| `apply` | Применить изменения из `jst.config.js` (labels + deps + ci) |
|
|
95
114
|
| `setup-labels` | Настроить GitHub labels |
|
|
96
|
-
| `setup-deps` | Настроить dependabot/renovate |
|
|
115
|
+
| `setup-deps` | Настроить dependabot/renovate (+ auto-merge workflow) |
|
|
116
|
+
| `setup-ci` | Сгенерировать `.github/workflows/ci.yml` |
|
|
117
|
+
| `setup-branch-protection` | Настроить защиту главной ветки через `gh api` |
|
|
97
118
|
|
|
98
119
|
### Разработка
|
|
99
120
|
|
|
@@ -219,6 +240,33 @@ npm run _ push-release
|
|
|
219
240
|
- [Changelogen](https://github.com/unjs/changelogen) — Генерация changelog
|
|
220
241
|
- [GitHub CLI](https://cli.github.com/) — Управление issues
|
|
221
242
|
|
|
243
|
+
## Migration Guide
|
|
244
|
+
|
|
245
|
+
### v1.9.0 — `release.demo` (breaking change)
|
|
246
|
+
|
|
247
|
+
Настройки демо переехали из плоского `release.*` в объект `release.demo`:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// ❌ Было (до v1.9.0)
|
|
251
|
+
release: {
|
|
252
|
+
requireDemo: true,
|
|
253
|
+
demoDir: 'docs',
|
|
254
|
+
demoFormats: ['gif', 'png'],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ✅ Стало
|
|
258
|
+
release: {
|
|
259
|
+
demo: {
|
|
260
|
+
enable: true,
|
|
261
|
+
dir: 'docs',
|
|
262
|
+
formats: ['gif', 'png'],
|
|
263
|
+
style: 'click', // новая опция: 'click' | 'side-by-side'
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Если в `jst.config.js` остались старые ключи, при запуске любой `jst`-команды появится предупреждение с инструкцией по миграции.
|
|
269
|
+
|
|
222
270
|
## Лицензия
|
|
223
271
|
|
|
224
272
|
MIT © [vv0rkz](https://github.com/vv0rkz)
|
package/bin/cli.js
CHANGED
|
@@ -101,6 +101,14 @@ const commands = {
|
|
|
101
101
|
spawnSync('node', [join(toolsDir, 'setup-deps.js')], { stdio: 'inherit' })
|
|
102
102
|
},
|
|
103
103
|
|
|
104
|
+
'setup-ci': () => {
|
|
105
|
+
spawnSync('node', [join(toolsDir, 'setup-ci.js')], { stdio: 'inherit' })
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
'setup-branch-protection': () => {
|
|
109
|
+
spawnSync('node', [join(toolsDir, 'setup-branch-protection.js')], { stdio: 'inherit' })
|
|
110
|
+
},
|
|
111
|
+
|
|
104
112
|
upgrade: () => {
|
|
105
113
|
spawnSync('node', [join(toolsDir, 'upgrade.js')], { stdio: 'inherit' })
|
|
106
114
|
},
|
|
@@ -109,7 +117,10 @@ const commands = {
|
|
|
109
117
|
console.log('⚙️ Применение jst.config.js...\n')
|
|
110
118
|
spawnSync('node', [join(toolsDir, 'setup-labels.js')], { stdio: 'inherit' })
|
|
111
119
|
spawnSync('node', [join(toolsDir, 'setup-deps.js')], { stdio: 'inherit' })
|
|
120
|
+
spawnSync('node', [join(toolsDir, 'setup-ci.js')], { stdio: 'inherit' })
|
|
112
121
|
console.log('\n✅ Конфиг применён!')
|
|
122
|
+
console.log('💡 Branch protection не применяется автоматически (требует admin прав).')
|
|
123
|
+
console.log(' Запусти вручную: npm run _ setup-branch-protection')
|
|
113
124
|
},
|
|
114
125
|
|
|
115
126
|
'pr-list': () => {
|
|
@@ -175,6 +186,8 @@ if (commands[command]) {
|
|
|
175
186
|
init-readme Создать стартовый README.md
|
|
176
187
|
setup-labels Настроить GitHub labels
|
|
177
188
|
setup-deps Настроить dependabot/renovate
|
|
189
|
+
setup-ci Сгенерировать .github/workflows/ci.yml
|
|
190
|
+
setup-branch-protection Настроить защиту главной ветки (gh api)
|
|
178
191
|
|
|
179
192
|
🔧 РАЗРАБОТКА:
|
|
180
193
|
update-readme Обновить README с версией
|
package/package.json
CHANGED
package/templates/jst.config.js
CHANGED
|
@@ -121,12 +121,55 @@ export default {
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Автообновление зависимостей
|
|
124
|
-
* @type {'dependabot' | 'renovate' | false}
|
|
125
124
|
*
|
|
126
|
-
*
|
|
127
|
-
* '
|
|
128
|
-
*
|
|
125
|
+
* Короткая форма:
|
|
126
|
+
* depUpdater: 'dependabot' — создаст .github/dependabot.yml
|
|
127
|
+
* depUpdater: 'renovate' — создаст renovate.json
|
|
128
|
+
* depUpdater: false — отключено
|
|
129
|
+
*
|
|
130
|
+
* Расширенная форма (только dependabot):
|
|
131
|
+
* depUpdater: {
|
|
132
|
+
* type: 'dependabot',
|
|
133
|
+
* ignoreMajor: true, // не открывать major PR-ы
|
|
134
|
+
* ignore: { major: true, minor: false }, // или более гранулярно
|
|
135
|
+
* autoMerge: { patch: true, minor: true, major: false },
|
|
136
|
+
* }
|
|
137
|
+
*
|
|
138
|
+
* При `autoMerge` jst дополнительно создаёт
|
|
139
|
+
* .github/workflows/dependabot-auto-merge.yml
|
|
129
140
|
*/
|
|
130
141
|
depUpdater: false,
|
|
131
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Continuous Integration (GitHub Actions)
|
|
145
|
+
*
|
|
146
|
+
* При `enable: true` команда `jst setup-ci` (и `jst apply`) создаёт
|
|
147
|
+
* .github/workflows/ci.yml — workflow с по одной job на каждый check.
|
|
148
|
+
* Имя job совпадает с именем npm-скрипта, который оно запускает.
|
|
149
|
+
*
|
|
150
|
+
* ci.yml: jobs.lint → runs `npm run lint`
|
|
151
|
+
* jobs.test → runs `npm run test`
|
|
152
|
+
*/
|
|
153
|
+
ci: {
|
|
154
|
+
enable: false,
|
|
155
|
+
checks: ['lint', 'test'],
|
|
156
|
+
nodeVersion: '20',
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Branch protection (через `gh api`)
|
|
161
|
+
*
|
|
162
|
+
* Команда `jst setup-branch-protection` настраивает защиту главной ветки
|
|
163
|
+
* через GitHub API. Не запускается автоматически в `jst apply` —
|
|
164
|
+
* требует admin прав на репозитории.
|
|
165
|
+
*
|
|
166
|
+
* requiredChecks — список обязательных status checks (должны совпадать
|
|
167
|
+
* с именами job-ов из ci.checks).
|
|
168
|
+
*/
|
|
169
|
+
branchProtection: {
|
|
170
|
+
enable: false,
|
|
171
|
+
requiredChecks: [],
|
|
172
|
+
enforceAdmins: false,
|
|
173
|
+
},
|
|
174
|
+
|
|
132
175
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from 'fs'
|
|
3
|
-
import { execSync } from 'child_process'
|
|
4
3
|
import { loadConfig } from './config.js'
|
|
4
|
+
import { predictNextVersion } from './version-utils.js'
|
|
5
5
|
|
|
6
6
|
const config = await loadConfig()
|
|
7
7
|
|
|
@@ -11,16 +11,7 @@ if (!config.release.demo.enable) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const commitMessages = execSync('git log --oneline -10', { encoding: 'utf8' })
|
|
17
|
-
|
|
18
|
-
let nextVersion
|
|
19
|
-
if (commitMessages.includes('feat:')) {
|
|
20
|
-
nextVersion = `v${major}.${minor + 1}.0`
|
|
21
|
-
} else {
|
|
22
|
-
nextVersion = `v${major}.${minor}.${patch + 1}`
|
|
23
|
-
}
|
|
14
|
+
const nextVersion = predictNextVersion(packageJson.version)
|
|
24
15
|
|
|
25
16
|
console.log(`📦 Предполагаемая следующая версия: ${nextVersion}`)
|
|
26
17
|
|
package/tools-gh/config.js
CHANGED
|
@@ -35,6 +35,28 @@ const DEFAULTS = {
|
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
depUpdater: false,
|
|
38
|
+
ci: {
|
|
39
|
+
enable: false,
|
|
40
|
+
checks: ['lint', 'test'],
|
|
41
|
+
nodeVersion: '20',
|
|
42
|
+
},
|
|
43
|
+
branchProtection: {
|
|
44
|
+
enable: false,
|
|
45
|
+
requiredChecks: [],
|
|
46
|
+
enforceAdmins: false,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes `depUpdater` to an object form.
|
|
52
|
+
* false → { type: false }
|
|
53
|
+
* 'dependabot' → { type: 'dependabot' }
|
|
54
|
+
* { type, ...opts } → unchanged
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeDepUpdater(depUpdater) {
|
|
57
|
+
if (!depUpdater) return { type: false }
|
|
58
|
+
if (typeof depUpdater === 'string') return { type: depUpdater }
|
|
59
|
+
return { type: false, ...depUpdater }
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
// --- Branch pattern template engine ---
|
|
@@ -100,6 +122,35 @@ function deepMerge(target, source) {
|
|
|
100
122
|
return result
|
|
101
123
|
}
|
|
102
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Checks for deprecated config keys and prints migration warnings.
|
|
127
|
+
* Old flat release.* keys were moved to release.demo.* in v1.9.0.
|
|
128
|
+
*/
|
|
129
|
+
function warnDeprecatedKeys(userConfig) {
|
|
130
|
+
const deprecated = [
|
|
131
|
+
{ old: 'release.requireDemo', newKey: 'release.demo.enable' },
|
|
132
|
+
{ old: 'release.demoDir', newKey: 'release.demo.dir' },
|
|
133
|
+
{ old: 'release.demoFormats', newKey: 'release.demo.formats' },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
const found = deprecated.filter(({ old }) => {
|
|
137
|
+
const [top, key] = old.split('.')
|
|
138
|
+
return userConfig[top]?.[key] !== undefined
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (found.length === 0) return
|
|
142
|
+
|
|
143
|
+
console.warn('\n⚠️ jst.config.js: устаревшие настройки (обнови конфиг)\n')
|
|
144
|
+
found.forEach(({ old, newKey }) => {
|
|
145
|
+
console.warn(` ${old} → ${newKey}`)
|
|
146
|
+
})
|
|
147
|
+
console.warn('\n Было:')
|
|
148
|
+
console.warn(' release: { requireDemo, demoDir, demoFormats }')
|
|
149
|
+
console.warn('\n Стало (начиная с v1.9.0):')
|
|
150
|
+
console.warn(' release: { demo: { enable, dir, formats, style } }')
|
|
151
|
+
console.warn('\n Запусти: npm run _ upgrade — чтобы получить актуальный конфиг\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
103
154
|
/**
|
|
104
155
|
* Loads config from jst.config.js (preferred) or jst.config.json (fallback)
|
|
105
156
|
* Deep-merges user config with defaults
|
|
@@ -124,5 +175,7 @@ export async function loadConfig() {
|
|
|
124
175
|
}
|
|
125
176
|
}
|
|
126
177
|
|
|
178
|
+
warnDeprecatedKeys(userConfig)
|
|
179
|
+
|
|
127
180
|
return deepMerge(DEFAULTS, userConfig)
|
|
128
181
|
}
|
|
@@ -5,8 +5,13 @@ import { loadConfig } from './config.js'
|
|
|
5
5
|
const config = await loadConfig()
|
|
6
6
|
const { closeKeyword, requireIssue } = config.commits
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function safeExec(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
|
11
|
+
} catch {
|
|
12
|
+
return ''
|
|
13
|
+
}
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
let repoUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim()
|
|
12
17
|
if (repoUrl.startsWith('git@github.com:')) {
|
|
@@ -15,26 +20,56 @@ if (repoUrl.startsWith('git@github.com:')) {
|
|
|
15
20
|
repoUrl = repoUrl.replace(/\.git$/, '')
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
// Capture upstream BEFORE push so we can scan everything that lands on the
|
|
24
|
+
// remote in this push — including older commits whose pushes were previously
|
|
25
|
+
// rejected by pre-push (fixes #10).
|
|
26
|
+
const beforeUpstream = safeExec('git rev-parse @{u}')
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
execSync('git push -u origin HEAD', { stdio: 'inherit' })
|
|
30
|
+
} catch {
|
|
31
|
+
// pre-push hook may have rejected the push; nothing to close yet.
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// On successful push, scan every commit between the previously known remote
|
|
36
|
+
// HEAD and the new HEAD for close-keywords. If we had no upstream before,
|
|
37
|
+
// fall back to the last 50 commits.
|
|
38
|
+
const range = beforeUpstream ? `${beforeUpstream}..HEAD` : '-50'
|
|
39
|
+
const log = safeExec(`git log ${range} --format=%H%x09%s`)
|
|
40
|
+
|
|
41
|
+
if (!log) process.exit(0)
|
|
19
42
|
|
|
20
43
|
const escClose = closeKeyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
21
44
|
const typesGroup = requireIssue.join('|')
|
|
22
45
|
const closePattern = new RegExp(`^(${typesGroup})(\\(.+\\))?: ${escClose} #(\\d+)`)
|
|
23
|
-
const match = commitMsg.match(closePattern)
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
const seen = new Set()
|
|
48
|
+
|
|
49
|
+
for (const line of log.split('\n').filter(Boolean)) {
|
|
50
|
+
const tabIndex = line.indexOf('\t')
|
|
51
|
+
if (tabIndex === -1) continue
|
|
52
|
+
const hash = line.slice(0, tabIndex)
|
|
53
|
+
const subject = line.slice(tabIndex + 1)
|
|
54
|
+
|
|
55
|
+
const match = subject.match(closePattern)
|
|
56
|
+
if (!match) continue
|
|
57
|
+
|
|
26
58
|
const issueNumber = match[3]
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
if (seen.has(issueNumber)) continue
|
|
60
|
+
seen.add(issueNumber)
|
|
61
|
+
|
|
62
|
+
const shortHash = hash.slice(0, 7)
|
|
63
|
+
const commitLink = `${repoUrl}/commit/${hash}`
|
|
29
64
|
|
|
30
|
-
console.log(`🔧 Закрываю issue #${issueNumber}...`)
|
|
65
|
+
console.log(`🔧 Закрываю issue #${issueNumber} (коммит ${shortHash})...`)
|
|
31
66
|
|
|
32
67
|
try {
|
|
33
68
|
execSync(
|
|
34
|
-
`gh issue close "${issueNumber}" --comment "✅ Завершено в коммите: [${
|
|
69
|
+
`gh issue close "${issueNumber}" --comment "✅ Завершено в коммите: [${shortHash}](${commitLink}) - ${subject}"`,
|
|
35
70
|
{ stdio: 'inherit' },
|
|
36
71
|
)
|
|
37
72
|
} catch {
|
|
38
|
-
console.log(`⚠️ Не удалось закрыть issue #${issueNumber}`)
|
|
73
|
+
console.log(`⚠️ Не удалось закрыть issue #${issueNumber} (возможно уже закрыт)`)
|
|
39
74
|
}
|
|
40
75
|
}
|
package/tools-gh/release.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from 'child_process'
|
|
3
3
|
import { existsSync, readFileSync } from 'fs'
|
|
4
4
|
import { platform } from 'os'
|
|
5
5
|
import { dirname, join } from 'path'
|
|
6
6
|
import { fileURLToPath } from 'url'
|
|
7
7
|
import { loadConfig } from './config.js'
|
|
8
|
+
import { getCommitsSinceLastTag, getLastTag, predictNextVersion } from './version-utils.js'
|
|
8
9
|
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
11
|
const isWin = platform() === 'win32'
|
|
@@ -23,12 +24,7 @@ console.log(`📦 Текущая версия: ${currentVersion}`)
|
|
|
23
24
|
|
|
24
25
|
// 2. Demo check (configurable)
|
|
25
26
|
if (config.release.demo.enable) {
|
|
26
|
-
const
|
|
27
|
-
const recentCommits = execSync('git log --oneline -10', { encoding: 'utf8' })
|
|
28
|
-
const nextVersion = recentCommits.includes('feat:')
|
|
29
|
-
? `v${major}.${minor + 1}.0`
|
|
30
|
-
: `v${major}.${minor}.${patch + 1}`
|
|
31
|
-
|
|
27
|
+
const nextVersion = predictNextVersion(currentVersion)
|
|
32
28
|
console.log(`📦 Предполагаемая следующая версия: ${nextVersion}`)
|
|
33
29
|
|
|
34
30
|
const { dir: demoDir, formats: demoFormats } = config.release.demo
|
|
@@ -58,19 +54,11 @@ if (currentVersion.startsWith('0.')) {
|
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
// 4. Commit analysis
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log(`📌 Последний тег: ${lastTag}`)
|
|
65
|
-
} catch {
|
|
66
|
-
console.log('📌 Теги не найдены (первый релиз)')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const commitLog = lastTag
|
|
70
|
-
? execSync(`git log ${lastTag}..HEAD --format=%s`, { encoding: 'utf8' })
|
|
71
|
-
: execSync('git log --format=%s -10', { encoding: 'utf8' })
|
|
57
|
+
const lastTag = getLastTag()
|
|
58
|
+
if (lastTag) console.log(`📌 Последний тег: ${lastTag}`)
|
|
59
|
+
else console.log('📌 Теги не найдены (первый релиз)')
|
|
72
60
|
|
|
73
|
-
const commits =
|
|
61
|
+
const commits = getCommitsSinceLastTag(lastTag)
|
|
74
62
|
const countByType = (prefix) => commits.filter((c) => c.startsWith(`${prefix}:`)).length
|
|
75
63
|
|
|
76
64
|
const featCount = countByType('feat')
|
package/tools-gh/report-issue.js
CHANGED
|
@@ -6,25 +6,41 @@ const jstRepo = 'vv0rkz/js-template'
|
|
|
6
6
|
const isWin = platform() === 'win32'
|
|
7
7
|
const ghCmd = isWin ? 'gh.exe' : 'gh'
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const rawArgs = process.argv.slice(2)
|
|
10
10
|
|
|
11
11
|
console.log(`📝 Создаю issue в ${jstRepo}...`)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// If the caller passed any flag (anything starting with `-`), we assume a
|
|
14
|
+
// non-interactive invocation and forward every argument verbatim to
|
|
15
|
+
// `gh issue create`. This enables script/CI usage like:
|
|
16
|
+
// jst report-issue --title "..." --body-file body.md --label bug
|
|
17
|
+
//
|
|
18
|
+
// Otherwise we keep the historical shorthand and join the positional args
|
|
19
|
+
// into the title:
|
|
20
|
+
// jst report-issue "Quick bug description"
|
|
21
|
+
const hasFlags = rawArgs.some((a) => a.startsWith('-'))
|
|
22
|
+
|
|
23
|
+
let ghArgs
|
|
24
|
+
if (rawArgs.length === 0) {
|
|
25
|
+
ghArgs = ['issue', 'create', '--repo', jstRepo]
|
|
26
|
+
} else if (hasFlags) {
|
|
27
|
+
ghArgs = ['issue', 'create', '--repo', jstRepo, ...rawArgs]
|
|
28
|
+
} else {
|
|
29
|
+
ghArgs = ['issue', 'create', '--repo', jstRepo, '--title', rawArgs.join(' ')]
|
|
16
30
|
}
|
|
17
31
|
|
|
18
|
-
const result = spawnSync(ghCmd,
|
|
19
|
-
stdio: 'inherit',
|
|
20
|
-
})
|
|
32
|
+
const result = spawnSync(ghCmd, ghArgs, { stdio: 'inherit' })
|
|
21
33
|
|
|
22
34
|
if (result.status === 0) {
|
|
23
35
|
console.log('\n✅ Issue создан!')
|
|
24
|
-
|
|
25
|
-
console.error('❌ Ошибка создания issue')
|
|
26
|
-
console.log('\n💡 Убедись что:')
|
|
27
|
-
console.log(' 1. Установлен GitHub CLI: gh --version')
|
|
28
|
-
console.log(' 2. Выполнена авторизация: gh auth login')
|
|
29
|
-
process.exit(1)
|
|
36
|
+
process.exit(0)
|
|
30
37
|
}
|
|
38
|
+
|
|
39
|
+
console.error('❌ Ошибка создания issue')
|
|
40
|
+
console.log('\n💡 Убедись что:')
|
|
41
|
+
console.log(' 1. Установлен GitHub CLI: gh --version')
|
|
42
|
+
console.log(' 2. Выполнена авторизация: gh auth login')
|
|
43
|
+
console.log(' 3. Для non-interactive вызова используй флаги:')
|
|
44
|
+
console.log(' jst report-issue --title "..." --body "..."')
|
|
45
|
+
console.log(' jst report-issue --title "..." --body-file body.md')
|
|
46
|
+
process.exit(result.status || 1)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process'
|
|
3
|
+
import { platform } from 'os'
|
|
4
|
+
import { loadConfig } from './config.js'
|
|
5
|
+
|
|
6
|
+
const config = await loadConfig()
|
|
7
|
+
const bp = config.branchProtection
|
|
8
|
+
|
|
9
|
+
if (!bp || !bp.enable) {
|
|
10
|
+
console.log('⏭️ Branch protection отключена (branchProtection.enable = false)')
|
|
11
|
+
console.log('💡 Включи в jst.config.js: branchProtection: { enable: true, requiredChecks: [...] }')
|
|
12
|
+
process.exit(0)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const requiredChecks = Array.isArray(bp.requiredChecks) ? bp.requiredChecks.filter(Boolean) : []
|
|
16
|
+
const enforceAdmins = !!bp.enforceAdmins
|
|
17
|
+
const mainBranch = (config.branch && config.branch.main) || 'main'
|
|
18
|
+
|
|
19
|
+
const isWin = platform() === 'win32'
|
|
20
|
+
const ghCmd = isWin ? 'gh.exe' : 'gh'
|
|
21
|
+
|
|
22
|
+
console.log(`🔒 Настройка branch protection для ветки "${mainBranch}"...\n`)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
execSync(`${ghCmd} auth status`, { stdio: 'ignore' })
|
|
26
|
+
} catch {
|
|
27
|
+
console.error('❌ GitHub CLI не установлен или не авторизован')
|
|
28
|
+
console.log('💡 Установи: https://cli.github.com/')
|
|
29
|
+
console.log(' И выполни: gh auth login')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let repoSlug = ''
|
|
34
|
+
try {
|
|
35
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf8' }).trim()
|
|
36
|
+
const match = remote.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/)
|
|
37
|
+
if (match) repoSlug = match[1]
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!repoSlug) {
|
|
43
|
+
console.error('❌ Не удалось определить owner/repo из git remote origin')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const payload = {
|
|
48
|
+
required_status_checks:
|
|
49
|
+
requiredChecks.length > 0 ? { strict: true, contexts: requiredChecks } : null,
|
|
50
|
+
enforce_admins: enforceAdmins,
|
|
51
|
+
required_pull_request_reviews: null,
|
|
52
|
+
restrictions: null,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`📍 Repo: ${repoSlug}`)
|
|
56
|
+
console.log(`📍 Required checks: ${requiredChecks.length ? requiredChecks.join(', ') : '(none)'}`)
|
|
57
|
+
console.log(`📍 Enforce admins: ${enforceAdmins}`)
|
|
58
|
+
console.log()
|
|
59
|
+
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
ghCmd,
|
|
62
|
+
['api', '-X', 'PUT', `repos/${repoSlug}/branches/${mainBranch}/protection`, '--input', '-'],
|
|
63
|
+
{
|
|
64
|
+
input: JSON.stringify(payload),
|
|
65
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (result.status === 0) {
|
|
71
|
+
console.log('\n✅ Branch protection настроена!')
|
|
72
|
+
} else {
|
|
73
|
+
console.error('\n❌ Не удалось настроить branch protection')
|
|
74
|
+
console.log('💡 Возможные причины:')
|
|
75
|
+
console.log(' • Нет прав admin на репозиторий')
|
|
76
|
+
console.log(' • Репозиторий приватный на бесплатном плане (нужен Pro/Team/Enterprise)')
|
|
77
|
+
console.log(' • Указанные required checks ещё не запускались на этой ветке')
|
|
78
|
+
process.exit(result.status || 1)
|
|
79
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { loadConfig } from './config.js'
|
|
5
|
+
|
|
6
|
+
const config = await loadConfig()
|
|
7
|
+
const ci = config.ci
|
|
8
|
+
|
|
9
|
+
if (!ci || !ci.enable) {
|
|
10
|
+
console.log('⏭️ CI отключён (ci.enable = false)')
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const checks = Array.isArray(ci.checks) ? ci.checks.filter(Boolean) : []
|
|
15
|
+
if (checks.length === 0) {
|
|
16
|
+
console.log('⚠️ ci.enable = true, но ci.checks пуст — workflow не создан')
|
|
17
|
+
process.exit(0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const nodeVersion = ci.nodeVersion || '20'
|
|
21
|
+
const mainBranch = (config.branch && config.branch.main) || 'main'
|
|
22
|
+
|
|
23
|
+
const workflowsDir = join(process.cwd(), '.github', 'workflows')
|
|
24
|
+
if (!existsSync(workflowsDir)) mkdirSync(workflowsDir, { recursive: true })
|
|
25
|
+
|
|
26
|
+
const filePath = join(workflowsDir, 'ci.yml')
|
|
27
|
+
if (existsSync(filePath)) {
|
|
28
|
+
console.log('⏭️ .github/workflows/ci.yml уже существует')
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const jobs = checks
|
|
33
|
+
.map(
|
|
34
|
+
(check) => ` ${check}:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: actions/setup-node@v4
|
|
39
|
+
with:
|
|
40
|
+
node-version: '${nodeVersion}'
|
|
41
|
+
cache: npm
|
|
42
|
+
- run: npm ci
|
|
43
|
+
- run: npm run ${check}`,
|
|
44
|
+
)
|
|
45
|
+
.join('\n\n')
|
|
46
|
+
|
|
47
|
+
const yaml = `name: CI
|
|
48
|
+
|
|
49
|
+
on:
|
|
50
|
+
push:
|
|
51
|
+
branches: [${mainBranch}]
|
|
52
|
+
pull_request:
|
|
53
|
+
branches: [${mainBranch}]
|
|
54
|
+
|
|
55
|
+
jobs:
|
|
56
|
+
${jobs}
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
writeFileSync(filePath, yaml)
|
|
60
|
+
console.log('✅ Создан .github/workflows/ci.yml')
|
|
61
|
+
console.log(` ↳ checks: ${checks.join(', ')}`)
|
|
62
|
+
console.log(` ↳ node: ${nodeVersion}`)
|
|
63
|
+
console.log(` ↳ branch: ${mainBranch}`)
|
package/tools-gh/setup-deps.js
CHANGED
|
@@ -1,43 +1,139 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
3
3
|
import { join } from 'path'
|
|
4
|
-
import { loadConfig } from './config.js'
|
|
4
|
+
import { loadConfig, normalizeDepUpdater } from './config.js'
|
|
5
5
|
|
|
6
6
|
const config = await loadConfig()
|
|
7
|
-
const
|
|
7
|
+
const dep = normalizeDepUpdater(config.depUpdater)
|
|
8
8
|
|
|
9
|
-
if (!
|
|
9
|
+
if (!dep.type) {
|
|
10
10
|
console.log('⏭️ Автообновление зависимостей отключено (depUpdater = false)')
|
|
11
11
|
process.exit(0)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
if (
|
|
14
|
+
if (dep.type === 'dependabot') {
|
|
15
|
+
writeDependabotConfig(dep)
|
|
16
|
+
if (dep.autoMerge) {
|
|
17
|
+
// shorthand: `autoMerge: true` → enable patch + minor auto-merge
|
|
18
|
+
const am = dep.autoMerge === true ? { patch: true, minor: true } : dep.autoMerge
|
|
19
|
+
writeDependabotAutoMergeWorkflow(am)
|
|
20
|
+
}
|
|
21
|
+
} else if (dep.type === 'renovate') {
|
|
22
|
+
writeRenovateConfig()
|
|
23
|
+
} else {
|
|
24
|
+
console.log(`⚠️ Неизвестный depUpdater.type: "${dep.type}"`)
|
|
25
|
+
console.log('💡 Допустимые значения: "dependabot", "renovate", false')
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- helpers ------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function buildIgnoreList(dep) {
|
|
32
|
+
// Granular `ignore: { major, minor, patch }` takes precedence over
|
|
33
|
+
// the simple `ignoreMajor` boolean.
|
|
34
|
+
const types = []
|
|
35
|
+
if (dep.ignore && typeof dep.ignore === 'object') {
|
|
36
|
+
if (dep.ignore.major) types.push('version-update:semver-major')
|
|
37
|
+
if (dep.ignore.minor) types.push('version-update:semver-minor')
|
|
38
|
+
if (dep.ignore.patch) types.push('version-update:semver-patch')
|
|
39
|
+
} else if (dep.ignoreMajor) {
|
|
40
|
+
types.push('version-update:semver-major')
|
|
41
|
+
}
|
|
42
|
+
return types
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeDependabotConfig(dep) {
|
|
15
46
|
const dir = join(process.cwd(), '.github')
|
|
16
47
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
17
48
|
|
|
18
49
|
const filePath = join(dir, 'dependabot.yml')
|
|
19
50
|
if (existsSync(filePath)) {
|
|
20
51
|
console.log('⏭️ .github/dependabot.yml уже существует')
|
|
21
|
-
|
|
52
|
+
return
|
|
22
53
|
}
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
const ignoreTypes = buildIgnoreList(dep)
|
|
56
|
+
const ignoreBlock = ignoreTypes.length
|
|
57
|
+
? ` ignore:\n - dependency-name: "*"\n update-types: [${ignoreTypes
|
|
58
|
+
.map((t) => `"${t}"`)
|
|
59
|
+
.join(', ')}]\n`
|
|
60
|
+
: ''
|
|
61
|
+
|
|
62
|
+
const yaml =
|
|
63
|
+
`version: 2\n` +
|
|
64
|
+
`updates:\n` +
|
|
65
|
+
` - package-ecosystem: "npm"\n` +
|
|
66
|
+
` directory: "/"\n` +
|
|
67
|
+
` schedule:\n` +
|
|
68
|
+
` interval: "weekly"\n` +
|
|
69
|
+
` open-pull-requests-limit: 10\n` +
|
|
70
|
+
ignoreBlock
|
|
71
|
+
|
|
72
|
+
writeFileSync(filePath, yaml)
|
|
35
73
|
console.log('✅ Создан .github/dependabot.yml')
|
|
36
|
-
|
|
74
|
+
if (ignoreTypes.length) {
|
|
75
|
+
console.log(` ↳ ignore update-types: ${ignoreTypes.join(', ')}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeDependabotAutoMergeWorkflow(autoMerge) {
|
|
80
|
+
const dir = join(process.cwd(), '.github', 'workflows')
|
|
81
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
82
|
+
|
|
83
|
+
const filePath = join(dir, 'dependabot-auto-merge.yml')
|
|
84
|
+
if (existsSync(filePath)) {
|
|
85
|
+
console.log('⏭️ .github/workflows/dependabot-auto-merge.yml уже существует')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const conditions = []
|
|
90
|
+
if (autoMerge.patch) conditions.push("steps.meta.outputs.update-type == 'version-update:semver-patch'")
|
|
91
|
+
if (autoMerge.minor) conditions.push("steps.meta.outputs.update-type == 'version-update:semver-minor'")
|
|
92
|
+
if (autoMerge.major) conditions.push("steps.meta.outputs.update-type == 'version-update:semver-major'")
|
|
93
|
+
|
|
94
|
+
if (conditions.length === 0) {
|
|
95
|
+
console.log('⏭️ depUpdater.autoMerge не содержит ни одного true — auto-merge workflow не создан')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ifClause = conditions.join(' || ')
|
|
100
|
+
|
|
101
|
+
const yaml = `name: Dependabot auto-merge
|
|
102
|
+
|
|
103
|
+
on: pull_request
|
|
104
|
+
|
|
105
|
+
permissions:
|
|
106
|
+
contents: write
|
|
107
|
+
pull-requests: write
|
|
108
|
+
|
|
109
|
+
jobs:
|
|
110
|
+
dependabot:
|
|
111
|
+
runs-on: ubuntu-latest
|
|
112
|
+
if: github.actor == 'dependabot[bot]'
|
|
113
|
+
steps:
|
|
114
|
+
- name: Fetch metadata
|
|
115
|
+
id: meta
|
|
116
|
+
uses: dependabot/fetch-metadata@v2
|
|
117
|
+
with:
|
|
118
|
+
github-token: "\${{ secrets.GITHUB_TOKEN }}"
|
|
119
|
+
|
|
120
|
+
- name: Enable auto-merge
|
|
121
|
+
if: ${ifClause}
|
|
122
|
+
run: gh pr merge --auto --squash "$PR_URL"
|
|
123
|
+
env:
|
|
124
|
+
PR_URL: \${{ github.event.pull_request.html_url }}
|
|
125
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
writeFileSync(filePath, yaml)
|
|
129
|
+
console.log('✅ Создан .github/workflows/dependabot-auto-merge.yml')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeRenovateConfig() {
|
|
37
133
|
const filePath = join(process.cwd(), 'renovate.json')
|
|
38
134
|
if (existsSync(filePath)) {
|
|
39
135
|
console.log('⏭️ renovate.json уже существует')
|
|
40
|
-
|
|
136
|
+
return
|
|
41
137
|
}
|
|
42
138
|
|
|
43
139
|
writeFileSync(
|
|
@@ -52,8 +148,4 @@ updates:
|
|
|
52
148
|
) + '\n',
|
|
53
149
|
)
|
|
54
150
|
console.log('✅ Создан renovate.json')
|
|
55
|
-
} else {
|
|
56
|
-
console.log(`⚠️ Неизвестный depUpdater: "${depUpdater}"`)
|
|
57
|
-
console.log('💡 Допустимые значения: "dependabot", "renovate", false')
|
|
58
|
-
process.exit(1)
|
|
59
151
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the last tag (e.g. "v1.8.4") or '' if no tags exist.
|
|
5
|
+
*/
|
|
6
|
+
export function getLastTag() {
|
|
7
|
+
try {
|
|
8
|
+
return execSync('git describe --tags --abbrev=0', {
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
11
|
+
}).trim()
|
|
12
|
+
} catch {
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns commit subjects since the last tag (or last 10 commits if no tag).
|
|
19
|
+
*/
|
|
20
|
+
export function getCommitsSinceLastTag(lastTag = getLastTag()) {
|
|
21
|
+
const log = lastTag
|
|
22
|
+
? execSync(`git log ${lastTag}..HEAD --format=%s`, { encoding: 'utf8' })
|
|
23
|
+
: execSync('git log --format=%s -10', { encoding: 'utf8' })
|
|
24
|
+
return log.split('\n').filter(Boolean)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Predicts the next semver version string ("vX.Y.Z") based on commits since the
|
|
29
|
+
* last tag. If any feat: commit is present → minor bump; otherwise patch bump.
|
|
30
|
+
*
|
|
31
|
+
* Uses commits since the last tag (not last 10 commits) so the prediction stays
|
|
32
|
+
* correct even after many docs:/refactor: commits land on top of older feat:s.
|
|
33
|
+
*/
|
|
34
|
+
export function predictNextVersion(currentVersion) {
|
|
35
|
+
const [major, minor, patch] = currentVersion.split('.').map(Number)
|
|
36
|
+
const commits = getCommitsSinceLastTag()
|
|
37
|
+
const hasFeat = commits.some((c) => /^feat(\(.+\))?!?:/.test(c))
|
|
38
|
+
|
|
39
|
+
return hasFeat ? `v${major}.${minor + 1}.0` : `v${major}.${minor}.${patch + 1}`
|
|
40
|
+
}
|