@vv0rkz/js-template 1.8.4 → 1.10.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 +35 -4
- package/bin/cli.js +44 -0
- package/package.json +1 -1
- package/templates/jst.config.js +64 -4
- package/tools-gh/check-demo-for-release.js +2 -11
- package/tools-gh/config.js +26 -0
- package/tools-gh/fix-releases.js +77 -0
- package/tools-gh/post-commit-hook.js +45 -10
- package/tools-gh/release-audit.js +103 -0
- 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
|
@@ -58,10 +58,33 @@ export default {
|
|
|
58
58
|
formats: ['gif', 'png'], // допустимые форматы
|
|
59
59
|
style: 'click', // 'click' | 'side-by-side'
|
|
60
60
|
},
|
|
61
|
+
// Pre-run хук: перед каждой jst-командой сравнивает теги ↔ Releases ↔
|
|
62
|
+
// демо ↔ README и выводит предупреждения (не блокирует команду).
|
|
63
|
+
audit: {
|
|
64
|
+
enable: true,
|
|
65
|
+
warnOnly: true,
|
|
66
|
+
},
|
|
61
67
|
},
|
|
62
68
|
|
|
63
69
|
// Автообновление зависимостей
|
|
64
|
-
|
|
70
|
+
// Короткая форма: 'dependabot' | 'renovate' | false
|
|
71
|
+
// Расширенная (только dependabot):
|
|
72
|
+
// { type: 'dependabot', ignoreMajor: true, autoMerge: { patch: true, minor: true } }
|
|
73
|
+
depUpdater: false,
|
|
74
|
+
|
|
75
|
+
// CI workflow (.github/workflows/ci.yml)
|
|
76
|
+
ci: {
|
|
77
|
+
enable: false, // включить генерацию ci.yml
|
|
78
|
+
checks: ['lint', 'test'], // имена job-ов = имена npm-скриптов
|
|
79
|
+
nodeVersion: '20',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Branch protection (через gh api)
|
|
83
|
+
branchProtection: {
|
|
84
|
+
enable: false,
|
|
85
|
+
requiredChecks: [], // должны совпадать с ci.checks
|
|
86
|
+
enforceAdmins: false,
|
|
87
|
+
},
|
|
65
88
|
}
|
|
66
89
|
```
|
|
67
90
|
|
|
@@ -74,8 +97,10 @@ export default {
|
|
|
74
97
|
| `release.*` | На следующем `jst release` |
|
|
75
98
|
| `labels` | После `jst setup-labels` или `jst apply` |
|
|
76
99
|
| `depUpdater` | После `jst setup-deps` или `jst apply` |
|
|
100
|
+
| `ci.*` | После `jst setup-ci` или `jst apply` |
|
|
101
|
+
| `branchProtection.*` | После `jst setup-branch-protection` (вручную) |
|
|
77
102
|
|
|
78
|
-
Короткий путь — `jst apply` применит labels + deps за раз.
|
|
103
|
+
Короткий путь — `jst apply` применит labels + deps + ci за раз. Branch protection требует admin-прав и запускается отдельно.
|
|
79
104
|
|
|
80
105
|
## Команды
|
|
81
106
|
|
|
@@ -91,9 +116,15 @@ npx jst <команда> # напрямую
|
|
|
91
116
|
|---|---|
|
|
92
117
|
| `init` | Инициализация нового проекта |
|
|
93
118
|
| `upgrade` | Обновить конфиги после `npm update` |
|
|
94
|
-
| `apply` | Применить изменения из `jst.config.js` |
|
|
119
|
+
| `apply` | Применить изменения из `jst.config.js` (labels + deps + ci) |
|
|
95
120
|
| `setup-labels` | Настроить GitHub labels |
|
|
96
|
-
| `setup-deps` | Настроить dependabot/renovate |
|
|
121
|
+
| `setup-deps` | Настроить dependabot/renovate (+ auto-merge workflow) |
|
|
122
|
+
| `setup-ci` | Сгенерировать `.github/workflows/ci.yml` |
|
|
123
|
+
| `setup-branch-protection` | Настроить защиту главной ветки через `gh api` |
|
|
124
|
+
| `audit` | Проверить теги/Releases/демо/README на пробелы |
|
|
125
|
+
| `fix-releases` | Создать GitHub Release для тегов без Release |
|
|
126
|
+
|
|
127
|
+
> Audit запускается автоматически перед каждой `jst`-командой (кроме `init`, `upgrade`, `audit`, `fix-releases`). Отключить разово — флагом `--no-audit`: `jst --no-audit create-task "..."`. Полностью — в `jst.config.js`: `release.audit.enable = false`.
|
|
97
128
|
|
|
98
129
|
### Разработка
|
|
99
130
|
|
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,13 @@ import { fileURLToPath } from 'url'
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
8
|
const toolsDir = join(__dirname, '../tools-gh')
|
|
9
9
|
|
|
10
|
+
// Strip the global `--no-audit` flag from argv before anyone else looks at it,
|
|
11
|
+
// so downstream command handlers (some of which read process.argv directly)
|
|
12
|
+
// don't see it as a positional argument.
|
|
13
|
+
const noAuditIndex = process.argv.indexOf('--no-audit')
|
|
14
|
+
const noAudit = noAuditIndex !== -1
|
|
15
|
+
if (noAudit) process.argv.splice(noAuditIndex, 1)
|
|
16
|
+
|
|
10
17
|
const args = process.argv.slice(2)
|
|
11
18
|
const command = args[0]
|
|
12
19
|
const commandArgs = args.slice(1)
|
|
@@ -15,6 +22,16 @@ const isWin = platform() === 'win32'
|
|
|
15
22
|
const npxCmd = isWin ? 'npx.cmd' : 'npx'
|
|
16
23
|
const ghCmd = isWin ? 'gh.exe' : 'gh'
|
|
17
24
|
|
|
25
|
+
// Commands that should NOT trigger the release audit pre-run hook.
|
|
26
|
+
// `init` runs before tags exist; `audit` / `fix-releases` ARE the audit
|
|
27
|
+
// itself; `upgrade` runs right after `npm update` and may pre-date tags.
|
|
28
|
+
const SKIP_AUDIT = new Set(['audit', 'fix-releases', 'init', 'upgrade', undefined])
|
|
29
|
+
|
|
30
|
+
function maybeRunAudit() {
|
|
31
|
+
if (noAudit || SKIP_AUDIT.has(command)) return
|
|
32
|
+
spawnSync('node', [join(toolsDir, 'release-audit.js'), '--quiet-if-clean'], { stdio: 'inherit' })
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
const commands = {
|
|
19
36
|
init: () => {
|
|
20
37
|
const initScript = join(__dirname, 'init.js')
|
|
@@ -101,6 +118,22 @@ const commands = {
|
|
|
101
118
|
spawnSync('node', [join(toolsDir, 'setup-deps.js')], { stdio: 'inherit' })
|
|
102
119
|
},
|
|
103
120
|
|
|
121
|
+
'setup-ci': () => {
|
|
122
|
+
spawnSync('node', [join(toolsDir, 'setup-ci.js')], { stdio: 'inherit' })
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
'setup-branch-protection': () => {
|
|
126
|
+
spawnSync('node', [join(toolsDir, 'setup-branch-protection.js')], { stdio: 'inherit' })
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
audit: () => {
|
|
130
|
+
spawnSync('node', [join(toolsDir, 'release-audit.js')], { stdio: 'inherit' })
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
'fix-releases': () => {
|
|
134
|
+
spawnSync('node', [join(toolsDir, 'fix-releases.js')], { stdio: 'inherit' })
|
|
135
|
+
},
|
|
136
|
+
|
|
104
137
|
upgrade: () => {
|
|
105
138
|
spawnSync('node', [join(toolsDir, 'upgrade.js')], { stdio: 'inherit' })
|
|
106
139
|
},
|
|
@@ -109,7 +142,10 @@ const commands = {
|
|
|
109
142
|
console.log('⚙️ Применение jst.config.js...\n')
|
|
110
143
|
spawnSync('node', [join(toolsDir, 'setup-labels.js')], { stdio: 'inherit' })
|
|
111
144
|
spawnSync('node', [join(toolsDir, 'setup-deps.js')], { stdio: 'inherit' })
|
|
145
|
+
spawnSync('node', [join(toolsDir, 'setup-ci.js')], { stdio: 'inherit' })
|
|
112
146
|
console.log('\n✅ Конфиг применён!')
|
|
147
|
+
console.log('💡 Branch protection не применяется автоматически (требует admin прав).')
|
|
148
|
+
console.log(' Запусти вручную: npm run _ setup-branch-protection')
|
|
113
149
|
},
|
|
114
150
|
|
|
115
151
|
'pr-list': () => {
|
|
@@ -161,6 +197,7 @@ const commands = {
|
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
if (commands[command]) {
|
|
200
|
+
maybeRunAudit()
|
|
164
201
|
commands[command]()
|
|
165
202
|
} else {
|
|
166
203
|
console.log(`
|
|
@@ -175,6 +212,10 @@ if (commands[command]) {
|
|
|
175
212
|
init-readme Создать стартовый README.md
|
|
176
213
|
setup-labels Настроить GitHub labels
|
|
177
214
|
setup-deps Настроить dependabot/renovate
|
|
215
|
+
setup-ci Сгенерировать .github/workflows/ci.yml
|
|
216
|
+
setup-branch-protection Настроить защиту главной ветки (gh api)
|
|
217
|
+
audit Проверить теги/Releases/демо/README на пробелы
|
|
218
|
+
fix-releases Создать GitHub Release для тегов без Release
|
|
178
219
|
|
|
179
220
|
🔧 РАЗРАБОТКА:
|
|
180
221
|
update-readme Обновить README с версией
|
|
@@ -205,6 +246,9 @@ if (commands[command]) {
|
|
|
205
246
|
⚙️ КОНФИГУРАЦИЯ:
|
|
206
247
|
jst.config.js Настройки веток, labels, коммитов, релизов
|
|
207
248
|
|
|
249
|
+
🚩 ГЛОБАЛЬНЫЕ ФЛАГИ:
|
|
250
|
+
--no-audit Отключить release audit для этого запуска
|
|
251
|
+
|
|
208
252
|
📝 ФОРМАТ КОММИТОВ:
|
|
209
253
|
feat: #9 описание Фича (ссылка на issue)
|
|
210
254
|
feat(scope): #9 описание Фича со scope
|
package/package.json
CHANGED
package/templates/jst.config.js
CHANGED
|
@@ -103,6 +103,23 @@ export default {
|
|
|
103
103
|
*/
|
|
104
104
|
style: 'click',
|
|
105
105
|
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Release audit — пассивная проверка перед каждой jst-командой.
|
|
109
|
+
* Сравнивает git tags ↔ GitHub Releases ↔ демо-файлы ↔ README-секции
|
|
110
|
+
* и печатает предупреждения, не блокируя команду.
|
|
111
|
+
*
|
|
112
|
+
* enable — включить хук (true | false)
|
|
113
|
+
* warnOnly — только предупреждать (true) или падать с кодом 1 (false)
|
|
114
|
+
*
|
|
115
|
+
* Отключить разово: `jst --no-audit <command>`
|
|
116
|
+
* Запустить явно: `jst audit`
|
|
117
|
+
* Починить пропуски: `jst fix-releases`
|
|
118
|
+
*/
|
|
119
|
+
audit: {
|
|
120
|
+
enable: true,
|
|
121
|
+
warnOnly: true,
|
|
122
|
+
},
|
|
106
123
|
},
|
|
107
124
|
|
|
108
125
|
/**
|
|
@@ -121,12 +138,55 @@ export default {
|
|
|
121
138
|
|
|
122
139
|
/**
|
|
123
140
|
* Автообновление зависимостей
|
|
124
|
-
* @type {'dependabot' | 'renovate' | false}
|
|
125
141
|
*
|
|
126
|
-
*
|
|
127
|
-
* '
|
|
128
|
-
*
|
|
142
|
+
* Короткая форма:
|
|
143
|
+
* depUpdater: 'dependabot' — создаст .github/dependabot.yml
|
|
144
|
+
* depUpdater: 'renovate' — создаст renovate.json
|
|
145
|
+
* depUpdater: false — отключено
|
|
146
|
+
*
|
|
147
|
+
* Расширенная форма (только dependabot):
|
|
148
|
+
* depUpdater: {
|
|
149
|
+
* type: 'dependabot',
|
|
150
|
+
* ignoreMajor: true, // не открывать major PR-ы
|
|
151
|
+
* ignore: { major: true, minor: false }, // или более гранулярно
|
|
152
|
+
* autoMerge: { patch: true, minor: true, major: false },
|
|
153
|
+
* }
|
|
154
|
+
*
|
|
155
|
+
* При `autoMerge` jst дополнительно создаёт
|
|
156
|
+
* .github/workflows/dependabot-auto-merge.yml
|
|
129
157
|
*/
|
|
130
158
|
depUpdater: false,
|
|
131
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Continuous Integration (GitHub Actions)
|
|
162
|
+
*
|
|
163
|
+
* При `enable: true` команда `jst setup-ci` (и `jst apply`) создаёт
|
|
164
|
+
* .github/workflows/ci.yml — workflow с по одной job на каждый check.
|
|
165
|
+
* Имя job совпадает с именем npm-скрипта, который оно запускает.
|
|
166
|
+
*
|
|
167
|
+
* ci.yml: jobs.lint → runs `npm run lint`
|
|
168
|
+
* jobs.test → runs `npm run test`
|
|
169
|
+
*/
|
|
170
|
+
ci: {
|
|
171
|
+
enable: false,
|
|
172
|
+
checks: ['lint', 'test'],
|
|
173
|
+
nodeVersion: '20',
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Branch protection (через `gh api`)
|
|
178
|
+
*
|
|
179
|
+
* Команда `jst setup-branch-protection` настраивает защиту главной ветки
|
|
180
|
+
* через GitHub API. Не запускается автоматически в `jst apply` —
|
|
181
|
+
* требует admin прав на репозитории.
|
|
182
|
+
*
|
|
183
|
+
* requiredChecks — список обязательных status checks (должны совпадать
|
|
184
|
+
* с именами job-ов из ci.checks).
|
|
185
|
+
*/
|
|
186
|
+
branchProtection: {
|
|
187
|
+
enable: false,
|
|
188
|
+
requiredChecks: [],
|
|
189
|
+
enforceAdmins: false,
|
|
190
|
+
},
|
|
191
|
+
|
|
132
192
|
}
|
|
@@ -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
|
@@ -25,6 +25,10 @@ const DEFAULTS = {
|
|
|
25
25
|
formats: ['gif', 'png'],
|
|
26
26
|
style: 'click',
|
|
27
27
|
},
|
|
28
|
+
audit: {
|
|
29
|
+
enable: true,
|
|
30
|
+
warnOnly: true,
|
|
31
|
+
},
|
|
28
32
|
},
|
|
29
33
|
changelog: {
|
|
30
34
|
types: {
|
|
@@ -35,6 +39,28 @@ const DEFAULTS = {
|
|
|
35
39
|
},
|
|
36
40
|
},
|
|
37
41
|
depUpdater: false,
|
|
42
|
+
ci: {
|
|
43
|
+
enable: false,
|
|
44
|
+
checks: ['lint', 'test'],
|
|
45
|
+
nodeVersion: '20',
|
|
46
|
+
},
|
|
47
|
+
branchProtection: {
|
|
48
|
+
enable: false,
|
|
49
|
+
requiredChecks: [],
|
|
50
|
+
enforceAdmins: false,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalizes `depUpdater` to an object form.
|
|
56
|
+
* false → { type: false }
|
|
57
|
+
* 'dependabot' → { type: 'dependabot' }
|
|
58
|
+
* { type, ...opts } → unchanged
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeDepUpdater(depUpdater) {
|
|
61
|
+
if (!depUpdater) return { type: false }
|
|
62
|
+
if (typeof depUpdater === 'string') return { type: depUpdater }
|
|
63
|
+
return { type: false, ...depUpdater }
|
|
38
64
|
}
|
|
39
65
|
|
|
40
66
|
// --- Branch pattern template engine ---
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process'
|
|
3
|
+
import { platform } from 'os'
|
|
4
|
+
|
|
5
|
+
const isWin = platform() === 'win32'
|
|
6
|
+
const ghCmd = isWin ? 'gh.exe' : 'gh'
|
|
7
|
+
|
|
8
|
+
function safeExec(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
|
11
|
+
} catch {
|
|
12
|
+
return ''
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
execSync(`${ghCmd} auth status`, { stdio: 'ignore' })
|
|
18
|
+
} catch {
|
|
19
|
+
console.error('❌ GitHub CLI не установлен или не авторизован')
|
|
20
|
+
console.log('💡 Установи: https://cli.github.com/ и выполни: gh auth login')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tagsRaw = safeExec('git tag --sort=creatordate')
|
|
25
|
+
const tags = tagsRaw
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((t) => t.trim())
|
|
28
|
+
.filter((t) => /^v\d+\.\d+\.\d+$/.test(t))
|
|
29
|
+
|
|
30
|
+
if (tags.length === 0) {
|
|
31
|
+
console.log('ℹ️ Тегов не найдено — нечего восстанавливать')
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const releasesJson = safeExec(`${ghCmd} release list --json tagName --limit 200`)
|
|
36
|
+
let releaseTags = new Set()
|
|
37
|
+
try {
|
|
38
|
+
releaseTags = new Set(JSON.parse(releasesJson || '[]').map((r) => r.tagName))
|
|
39
|
+
} catch {
|
|
40
|
+
console.error('❌ Не удалось распарсить список GitHub Releases')
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const missing = tags.filter((t) => !releaseTags.has(t))
|
|
45
|
+
|
|
46
|
+
if (missing.length === 0) {
|
|
47
|
+
console.log('✅ Все теги уже имеют GitHub Release')
|
|
48
|
+
process.exit(0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`🔧 Создаю GitHub Release для ${missing.length} тег(ов):`)
|
|
52
|
+
missing.forEach((t) => console.log(` • ${t}`))
|
|
53
|
+
console.log()
|
|
54
|
+
|
|
55
|
+
let created = 0
|
|
56
|
+
let failed = 0
|
|
57
|
+
|
|
58
|
+
for (const tag of missing) {
|
|
59
|
+
process.stdout.write(` ${tag} ... `)
|
|
60
|
+
const result = spawnSync(ghCmd, ['release', 'create', tag, '--generate-notes', '--title', tag], {
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (result.status === 0) {
|
|
66
|
+
console.log('✅')
|
|
67
|
+
created++
|
|
68
|
+
} else {
|
|
69
|
+
console.log('⚠️')
|
|
70
|
+
const errMsg = (result.stderr || '').trim().split('\n').slice(0, 2).join(' | ')
|
|
71
|
+
if (errMsg) console.log(` ${errMsg}`)
|
|
72
|
+
failed++
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`\n📊 Создано: ${created}, ошибок: ${failed}`)
|
|
77
|
+
if (failed > 0) process.exit(1)
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { existsSync, readFileSync } from 'fs'
|
|
4
|
+
import { platform } from 'os'
|
|
5
|
+
import { loadConfig } from './config.js'
|
|
6
|
+
|
|
7
|
+
const isWin = platform() === 'win32'
|
|
8
|
+
const ghCmd = isWin ? 'gh.exe' : 'gh'
|
|
9
|
+
|
|
10
|
+
const config = await loadConfig()
|
|
11
|
+
const audit = (config.release && config.release.audit) || {}
|
|
12
|
+
|
|
13
|
+
if (audit.enable === false) process.exit(0)
|
|
14
|
+
|
|
15
|
+
// When invoked as a pre-run hook we stay silent on a clean repo. Stand-alone
|
|
16
|
+
// invocations (`jst audit`) print a positive confirmation.
|
|
17
|
+
const quietIfClean = process.argv.includes('--quiet-if-clean')
|
|
18
|
+
const warnOnly = audit.warnOnly !== false
|
|
19
|
+
|
|
20
|
+
function safeExec(cmd) {
|
|
21
|
+
try {
|
|
22
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
|
23
|
+
} catch {
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tagsRaw = safeExec('git tag --sort=-creatordate')
|
|
29
|
+
const tags = tagsRaw
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map((t) => t.trim())
|
|
32
|
+
.filter((t) => /^v\d+\.\d+\.\d+$/.test(t))
|
|
33
|
+
|
|
34
|
+
if (tags.length === 0) process.exit(0)
|
|
35
|
+
|
|
36
|
+
let releaseTags = new Set()
|
|
37
|
+
const releasesJson = safeExec(`${ghCmd} release list --json tagName --limit 100`)
|
|
38
|
+
if (releasesJson) {
|
|
39
|
+
try {
|
|
40
|
+
releaseTags = new Set(JSON.parse(releasesJson).map((r) => r.tagName))
|
|
41
|
+
} catch {
|
|
42
|
+
// gh returned non-JSON — treat as "no info", skip the release check
|
|
43
|
+
releaseTags = null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const demoCfg = (config.release && config.release.demo) || {}
|
|
48
|
+
const demoEnabled = demoCfg.enable !== false
|
|
49
|
+
const demoDir = demoCfg.dir || 'docs'
|
|
50
|
+
const demoFormats = Array.isArray(demoCfg.formats) && demoCfg.formats.length ? demoCfg.formats : ['gif', 'png']
|
|
51
|
+
|
|
52
|
+
let readmeContent = ''
|
|
53
|
+
let hasAutoSection = false
|
|
54
|
+
if (existsSync('README.md')) {
|
|
55
|
+
readmeContent = readFileSync('README.md', 'utf8')
|
|
56
|
+
hasAutoSection = readmeContent.includes('<!-- AUTOGENERATED_SECTION START -->')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const noRelease = []
|
|
60
|
+
const noDemo = []
|
|
61
|
+
const noReadme = []
|
|
62
|
+
|
|
63
|
+
for (const tag of tags) {
|
|
64
|
+
if (releaseTags && !releaseTags.has(tag)) noRelease.push(tag)
|
|
65
|
+
|
|
66
|
+
if (demoEnabled) {
|
|
67
|
+
const hasDemo = demoFormats.some((fmt) => existsSync(`${demoDir}/${tag}.${fmt}`))
|
|
68
|
+
if (!hasDemo) noDemo.push(tag)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (hasAutoSection && !readmeContent.includes(`### 🟢 ${tag}`)) {
|
|
72
|
+
noReadme.push(tag)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const total = noRelease.length + noDemo.length + noReadme.length
|
|
77
|
+
|
|
78
|
+
if (total === 0) {
|
|
79
|
+
if (!quietIfClean) console.log('✅ Release audit: всё в порядке')
|
|
80
|
+
process.exit(0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Compact list helper — first N tags then "+M more"
|
|
84
|
+
function fmtTags(list, limit = 5) {
|
|
85
|
+
if (list.length <= limit) return list.join(', ')
|
|
86
|
+
return list.slice(0, limit).join(', ') + ` +${list.length - limit} more`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (noRelease.length) {
|
|
90
|
+
console.warn(`⚠️ Release audit: ${noRelease.length} тег(ов) без GitHub Release: ${fmtTags(noRelease)}`)
|
|
91
|
+
console.warn(` → Исправить: jst fix-releases`)
|
|
92
|
+
}
|
|
93
|
+
if (noDemo.length) {
|
|
94
|
+
console.warn(`⚠️ Release audit: ${noDemo.length} тег(ов) без демо: ${fmtTags(noDemo)}`)
|
|
95
|
+
console.warn(` → Создай файлы в ${demoDir}/<tag>.${demoFormats[0]}`)
|
|
96
|
+
}
|
|
97
|
+
if (noReadme.length) {
|
|
98
|
+
console.warn(`⚠️ Release audit: README не содержит секции: ${fmtTags(noReadme)}`)
|
|
99
|
+
console.warn(` → Исправить: jst update-readme`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!warnOnly) process.exit(1)
|
|
103
|
+
process.exit(0)
|
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
|
+
}
|