@goodandready/dsh-github-ops 0.6.8 → 0.6.10
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/CHANGELOG.md +12 -0
- package/lib/client.js +58 -2
- package/lib/events.js +13 -0
- package/lib/routes.js +7 -0
- package/lib/tools/changelog.js +261 -0
- package/lib/tools/community.js +238 -0
- package/lib/tools/deployments.js +154 -0
- package/lib/tools/register-core.js +26 -0
- package/lib/tools/register-ops.js +131 -0
- package/lib/tools/register-write.js +37 -0
- package/lib/tools/webhook-sim.js +289 -0
- package/lib/tools/write-files.js +14 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.10
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **GitHub Environments & Deployments Management (#122)**: Added `gh_environment_list` tool to inspect deployment environments, wait timers, and protection rules. Added `gh_deployment_list`, `gh_deployment_create`, and `gh_deployment_status_create` tools for full deployment lifecycle auditing and status updates.
|
|
7
|
+
- **Webhook Simulator & Event Emitter (#125)**: Added `gh_webhook_simulate` tool supporting canonical mock payloads for `issues`, `pull_request`, `workflow_run`, `release`, `deployment`, `deployment_status`, and `push` events, with live broadcast into local `SseEventManager` and optional HTTP delivery to webhook receivers.
|
|
8
|
+
|
|
9
|
+
## 0.6.9
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
- **Conventional Changelog Generator (#123)**: Added `gh_changelog_generate` tool parsing commit and PR history according to Conventional Commits standard (`feat:`, `fix:`, `perf:`, `refactor:`, `chore:`, etc.), detecting Breaking Changes (`!` and `BREAKING CHANGE:`), recommending SemVer bump (`major`, `minor`, `patch`), and outputting structured JSON and GitHub/CHANGELOG markdown. Added Changelog tab in repository panel UI.
|
|
13
|
+
- **GoodAndReady Community Scaffold (#126)**: Added `gh_community_scaffold` tool generating standardized community health files (`.github/ISSUE_TEMPLATE` bug report, feature request, security advisory; 5-section `.github/PULL_REQUEST_TEMPLATE.md`; `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`). Integrated `includeCommunityFiles` option into `gh_upload_project` and `uploadProject`.
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Features
|
package/lib/client.js
CHANGED
|
@@ -130,6 +130,9 @@ window.__ModuleLoader__.load({
|
|
|
130
130
|
panelSecurity: 'Security',
|
|
131
131
|
panelSecurityEmpty: 'No security alerts found.',
|
|
132
132
|
panelSecurityDisabled: 'Security features are disabled or token lacks security scope.',
|
|
133
|
+
panelChangelog: 'Changelog',
|
|
134
|
+
panelChangelogEmpty: 'No conventional changelog entries found.',
|
|
135
|
+
panelCopyMarkdown: 'Copy Markdown',
|
|
133
136
|
panelDiagnose: 'Diagnose',
|
|
134
137
|
panelDiagnosing: 'Diagnosing…',
|
|
135
138
|
panelDiagnoseAsk: 'Ask Agent to Diagnose & Fix',
|
|
@@ -257,6 +260,9 @@ window.__ModuleLoader__.load({
|
|
|
257
260
|
panelSecurity: '安全',
|
|
258
261
|
panelSecurityEmpty: '未发现安全警报。',
|
|
259
262
|
panelSecurityDisabled: '安全功能已禁用或令牌缺少安全权限。',
|
|
263
|
+
panelChangelog: '更新日志',
|
|
264
|
+
panelChangelogEmpty: '未找到常规提交更新日志记录。',
|
|
265
|
+
panelCopyMarkdown: '复制 Markdown',
|
|
260
266
|
panelDiagnose: '诊断',
|
|
261
267
|
panelDiagnosing: '诊断中…',
|
|
262
268
|
panelDiagnoseAsk: '请智能体诊断并修复',
|
|
@@ -870,6 +876,7 @@ window.__ModuleLoader__.load({
|
|
|
870
876
|
const [error, setError] = React.useState('')
|
|
871
877
|
const [loading, setLoading] = React.useState(false)
|
|
872
878
|
const [securityData, setSecurityData] = React.useState({ phase: 'idle', data: null })
|
|
879
|
+
const [changelogData, setChangelogData] = React.useState({ phase: 'idle', data: null })
|
|
873
880
|
const [diagnostics, setDiagnostics] = React.useState(null)
|
|
874
881
|
const [diagnoseLoadingId, setDiagnoseLoadingId] = React.useState(null)
|
|
875
882
|
const [expandedSteps, setExpandedSteps] = React.useState({})
|
|
@@ -921,6 +928,14 @@ window.__ModuleLoader__.load({
|
|
|
921
928
|
.catch((err) => { setSecurityData({ phase: 'error', data: null }); setError(String((err && err.message) || err)) })
|
|
922
929
|
}, [call, repo])
|
|
923
930
|
|
|
931
|
+
const loadChangelog = React.useCallback(() => {
|
|
932
|
+
if (!repo) return
|
|
933
|
+
setChangelogData({ phase: 'loading', data: null })
|
|
934
|
+
call({ what: 'changelog', repo })
|
|
935
|
+
.then((payload) => setChangelogData({ phase: 'ready', data: payload }))
|
|
936
|
+
.catch((err) => { setChangelogData({ phase: 'error', data: null }); setError(String((err && err.message) || err)) })
|
|
937
|
+
}, [call, repo])
|
|
938
|
+
|
|
924
939
|
const loadDiagnostics = (runId) => {
|
|
925
940
|
if (!repo || !runId) return
|
|
926
941
|
setDiagnoseLoadingId(runId)
|
|
@@ -957,8 +972,9 @@ window.__ModuleLoader__.load({
|
|
|
957
972
|
else if (tab === 'inbox') loadInbox()
|
|
958
973
|
else if (tab === 'matrix') loadMatrix()
|
|
959
974
|
else if (tab === 'security') loadSecurity()
|
|
975
|
+
else if (tab === 'changelog') loadChangelog()
|
|
960
976
|
else loadList(tab)
|
|
961
|
-
}, [repo, tab, loadTree, loadList, loadInbox, loadSecurity])
|
|
977
|
+
}, [repo, tab, loadTree, loadList, loadInbox, loadSecurity, loadChangelog])
|
|
962
978
|
|
|
963
979
|
const openFile = (entry) => {
|
|
964
980
|
if (entry.type === 'tree') { loadTree(entry.path); return }
|
|
@@ -1143,6 +1159,45 @@ window.__ModuleLoader__.load({
|
|
|
1143
1159
|
)
|
|
1144
1160
|
})()
|
|
1145
1161
|
|
|
1162
|
+
const changelogView = (() => {
|
|
1163
|
+
if (changelogData.phase === 'loading') return React.createElement('div', { className: 'gho-panel-muted', style: { padding: '8px 0' } }, t('panelLoading'))
|
|
1164
|
+
const data = changelogData.data
|
|
1165
|
+
if (!data || !data.markdown) return React.createElement('div', { className: 'gho-panel-muted', style: { padding: '8px 0' } }, t('panelChangelogEmpty'))
|
|
1166
|
+
|
|
1167
|
+
return React.createElement('div', { style: { padding: '4px 0' } },
|
|
1168
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', margin: '4px 0 8px' } },
|
|
1169
|
+
React.createElement('span', { className: 'gho-badge gho-badge-active' }, `Bump: ${data.recommendedBump || 'none'}`),
|
|
1170
|
+
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, data.version || ''),
|
|
1171
|
+
React.createElement('button', {
|
|
1172
|
+
type: 'button',
|
|
1173
|
+
className: 'gho-diagnose-btn',
|
|
1174
|
+
style: { marginLeft: 'auto' },
|
|
1175
|
+
onClick: () => {
|
|
1176
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
1177
|
+
navigator.clipboard.writeText(data.markdown || '').catch(() => {})
|
|
1178
|
+
}
|
|
1179
|
+
setCopiedPrompt(true)
|
|
1180
|
+
setTimeout(() => setCopiedPrompt(false), 3000)
|
|
1181
|
+
},
|
|
1182
|
+
}, copiedPrompt ? t('panelDiagnoseCopied') : t('panelCopyMarkdown')),
|
|
1183
|
+
),
|
|
1184
|
+
React.createElement('pre', {
|
|
1185
|
+
style: {
|
|
1186
|
+
whiteSpace: 'pre-wrap',
|
|
1187
|
+
fontFamily: 'monospace',
|
|
1188
|
+
fontSize: '11px',
|
|
1189
|
+
lineHeight: '1.4',
|
|
1190
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1191
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1192
|
+
borderRadius: '6px',
|
|
1193
|
+
padding: '8px',
|
|
1194
|
+
maxHeight: '360px',
|
|
1195
|
+
overflow: 'auto',
|
|
1196
|
+
},
|
|
1197
|
+
}, data.markdown),
|
|
1198
|
+
)
|
|
1199
|
+
})()
|
|
1200
|
+
|
|
1146
1201
|
return React.createElement('div', { className: 'gho-panel' },
|
|
1147
1202
|
header,
|
|
1148
1203
|
React.createElement('div', { className: 'gho-panel-tabs' },
|
|
@@ -1151,12 +1206,13 @@ window.__ModuleLoader__.load({
|
|
|
1151
1206
|
tabButton('pulls', t('panelPulls')),
|
|
1152
1207
|
tabButton('runs', t('panelRuns')),
|
|
1153
1208
|
tabButton('security', t('panelSecurity')),
|
|
1209
|
+
tabButton('changelog', t('panelChangelog')),
|
|
1154
1210
|
tabButton('inbox', t('panelInbox')),
|
|
1155
1211
|
tabButton('matrix', t('panelMatrix')),
|
|
1156
1212
|
React.createElement('button', { type: 'button', className: 'gho-panel-tab', onClick: () => { setRepo(''); setFile(null) } }, t('panelRepo')),
|
|
1157
1213
|
),
|
|
1158
1214
|
error ? React.createElement('div', { className: 'gho-panel-muted' }, error) : null,
|
|
1159
|
-
tab === 'tree' ? treeView : (tab === 'matrix' ? matrixView : (tab === 'security' ? securityView : list)),
|
|
1215
|
+
tab === 'tree' ? treeView : (tab === 'matrix' ? matrixView : (tab === 'security' ? securityView : (tab === 'changelog' ? changelogView : list))),
|
|
1160
1216
|
)
|
|
1161
1217
|
}
|
|
1162
1218
|
|
package/lib/events.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
let activeEventManager = null
|
|
3
|
+
|
|
4
|
+
export function setGlobalEventManager(mgr) {
|
|
5
|
+
activeEventManager = mgr
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getGlobalEventManager() {
|
|
9
|
+
return activeEventManager
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
// Server-Sent Events (SSE) notification manager for CI runs and PR events.
|
|
2
13
|
//
|
|
3
14
|
// Maintains client subscriptions, broadcasts real-time events, and runs an adaptive
|
|
@@ -21,6 +32,7 @@ export class SseEventManager {
|
|
|
21
32
|
this.lastRuns = new Map() // runId -> { status, conclusion }
|
|
22
33
|
this.lastPulls = new Map() // pullNumber -> { updated, state }
|
|
23
34
|
this.hasActiveWork = false
|
|
35
|
+
setGlobalEventManager(this)
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
get clientCount() {
|
|
@@ -197,5 +209,6 @@ export class SseEventManager {
|
|
|
197
209
|
this.clients.clear()
|
|
198
210
|
this.lastRuns.clear()
|
|
199
211
|
this.lastPulls.clear()
|
|
212
|
+
if (getGlobalEventManager() === this) setGlobalEventManager(null)
|
|
200
213
|
}
|
|
201
214
|
}
|
package/lib/routes.js
CHANGED
|
@@ -16,6 +16,7 @@ import { encodeRepoPath } from './github.js'
|
|
|
16
16
|
import { listIssues } from './tools/issues.js'
|
|
17
17
|
import { listRuns, diagnoseRun } from './tools/actions.js'
|
|
18
18
|
import { listSecurityAlerts } from './tools/security.js'
|
|
19
|
+
import { generateChangelog } from './tools/changelog.js'
|
|
19
20
|
import { listNotifications, repoTreeView } from './tools/panel-data.js'
|
|
20
21
|
import { mirrorStatus, syncMirror } from './tools/mirror.js'
|
|
21
22
|
import { matrixStatus } from './tools/matrix.js'
|
|
@@ -413,6 +414,12 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
413
414
|
const result = await listRuns(github, { ...repo, limit: 20 })
|
|
414
415
|
return sendJson(res, 200, { configured: true, items: result.runs })
|
|
415
416
|
}
|
|
417
|
+
if (what === 'changelog') {
|
|
418
|
+
const base = url.searchParams.get('base') || undefined
|
|
419
|
+
const head = url.searchParams.get('head') || 'HEAD'
|
|
420
|
+
const result = await generateChangelog(github, { ...repo, base, head })
|
|
421
|
+
return sendJson(res, 200, { configured: true, ...result })
|
|
422
|
+
}
|
|
416
423
|
if (what === 'security') {
|
|
417
424
|
const severity = url.searchParams.get('severity') || undefined
|
|
418
425
|
const result = await listSecurityAlerts(github, { ...repo, state, severity, limit: 50 })
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Conventional changelog generation and SemVer recommendation tools.
|
|
2
|
+
// Pure functions over GitHub client or commit arrays.
|
|
3
|
+
|
|
4
|
+
export const CATEGORIES = {
|
|
5
|
+
breaking: { key: 'breaking', title: '⚠️ Breaking Changes', order: 1 },
|
|
6
|
+
feat: { key: 'feat', title: '🚀 Features', order: 2 },
|
|
7
|
+
fix: { key: 'fix', title: '🐛 Bug Fixes', order: 3 },
|
|
8
|
+
perf: { key: 'perf', title: '⚡ Performance Improvements', order: 4 },
|
|
9
|
+
refactor: { key: 'refactor', title: '♻️ Code Refactoring', order: 5 },
|
|
10
|
+
docs: { key: 'docs', title: '📚 Documentation', order: 6 },
|
|
11
|
+
test: { key: 'test', title: '🧪 Tests', order: 7 },
|
|
12
|
+
maintenance: { key: 'maintenance', title: '🛠️ Maintenance', order: 8 },
|
|
13
|
+
other: { key: 'other', title: '📦 Other Changes', order: 9 },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TYPE_TO_CATEGORY = {
|
|
17
|
+
feat: 'feat',
|
|
18
|
+
feature: 'feat',
|
|
19
|
+
fix: 'fix',
|
|
20
|
+
bugfix: 'fix',
|
|
21
|
+
perf: 'perf',
|
|
22
|
+
refactor: 'refactor',
|
|
23
|
+
docs: 'docs',
|
|
24
|
+
doc: 'docs',
|
|
25
|
+
test: 'test',
|
|
26
|
+
tests: 'test',
|
|
27
|
+
chore: 'maintenance',
|
|
28
|
+
ci: 'maintenance',
|
|
29
|
+
build: 'maintenance',
|
|
30
|
+
deps: 'maintenance',
|
|
31
|
+
style: 'maintenance',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CONVENTIONAL_RE = /^([a-zA-Z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/
|
|
35
|
+
|
|
36
|
+
/** Parse a single commit message into conventional commit structure. */
|
|
37
|
+
export function parseConventionalCommit(rawMessage, sha = '') {
|
|
38
|
+
const full = String(rawMessage || '').trim()
|
|
39
|
+
const lines = full.split('\n').map((l) => l.trim())
|
|
40
|
+
const header = lines[0] || ''
|
|
41
|
+
const body = lines.slice(1).join('\n')
|
|
42
|
+
|
|
43
|
+
let type = 'other'
|
|
44
|
+
let scope = ''
|
|
45
|
+
let isBreaking = false
|
|
46
|
+
let subject = header
|
|
47
|
+
let breakingNote = ''
|
|
48
|
+
|
|
49
|
+
// Check for BREAKING CHANGE in body or footer
|
|
50
|
+
const breakingMatch = body.match(/BREAKING[- ]CHANGE:\s*([^\n]+)/i)
|
|
51
|
+
if (breakingMatch) {
|
|
52
|
+
isBreaking = true
|
|
53
|
+
breakingNote = breakingMatch[1].trim()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const match = header.match(CONVENTIONAL_RE)
|
|
57
|
+
if (match) {
|
|
58
|
+
type = match[1].toLowerCase()
|
|
59
|
+
scope = match[2] ? match[2].trim() : ''
|
|
60
|
+
if (match[3] === '!') isBreaking = true
|
|
61
|
+
subject = match[4].trim()
|
|
62
|
+
} else {
|
|
63
|
+
// Handle typical merge pull request headers e.g. "Merge pull request #123 from org/branch"
|
|
64
|
+
const prMatch = header.match(/^Merge pull request #(\d+)/i)
|
|
65
|
+
if (prMatch) {
|
|
66
|
+
const candidateLine = lines.slice(1).find((l) => l.trim().length > 0)
|
|
67
|
+
if (candidateLine) {
|
|
68
|
+
const subMatch = candidateLine.match(CONVENTIONAL_RE)
|
|
69
|
+
if (subMatch) {
|
|
70
|
+
type = subMatch[1].toLowerCase()
|
|
71
|
+
scope = subMatch[2] ? subMatch[2].trim() : ''
|
|
72
|
+
if (subMatch[3] === '!') isBreaking = true
|
|
73
|
+
subject = `${subMatch[4].trim()} (#${prMatch[1]})`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const categoryKey = isBreaking
|
|
80
|
+
? 'breaking'
|
|
81
|
+
: (TYPE_TO_CATEGORY[type] || 'other')
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
sha: sha ? sha.slice(0, 7) : '',
|
|
85
|
+
fullSha: sha || '',
|
|
86
|
+
raw: full,
|
|
87
|
+
header,
|
|
88
|
+
type,
|
|
89
|
+
scope,
|
|
90
|
+
subject,
|
|
91
|
+
isBreaking,
|
|
92
|
+
breakingNote,
|
|
93
|
+
category: categoryKey,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Calculate recommended SemVer bump from parsed commits. */
|
|
98
|
+
export function calculateBump(commits = []) {
|
|
99
|
+
if (!Array.isArray(commits) || commits.length === 0) return 'none'
|
|
100
|
+
if (commits.some((c) => c.isBreaking || c.category === 'breaking')) return 'major'
|
|
101
|
+
if (commits.some((c) => c.type === 'feat' || c.category === 'feat')) return 'minor'
|
|
102
|
+
return 'patch'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Bump a SemVer version string according to bump type ('major', 'minor', 'patch'). */
|
|
106
|
+
export function bumpVersion(version, bump) {
|
|
107
|
+
if (!version || bump === 'none') return version || '0.1.0'
|
|
108
|
+
const hasV = version.startsWith('v')
|
|
109
|
+
const clean = hasV ? version.slice(1) : version
|
|
110
|
+
const parts = clean.split('-')[0].split('.').map(Number)
|
|
111
|
+
let [major = 0, minor = 0, patch = 0] = parts
|
|
112
|
+
|
|
113
|
+
if (bump === 'major') {
|
|
114
|
+
major += 1
|
|
115
|
+
minor = 0
|
|
116
|
+
patch = 0
|
|
117
|
+
} else if (bump === 'minor') {
|
|
118
|
+
minor += 1
|
|
119
|
+
patch = 0
|
|
120
|
+
} else if (bump === 'patch') {
|
|
121
|
+
patch += 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const res = `${major}.${minor}.${patch}`
|
|
125
|
+
return hasV ? `v${res}` : res
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Format parsed commits into structured markdown changelog. */
|
|
129
|
+
export function formatChangelog({ version = '', date = '', sections = {}, compareUrl = '' } = {}) {
|
|
130
|
+
const headerDate = date || new Date().toISOString().slice(0, 10)
|
|
131
|
+
const lines = []
|
|
132
|
+
if (version) {
|
|
133
|
+
lines.push(`## [${version}] - ${headerDate}`)
|
|
134
|
+
} else {
|
|
135
|
+
lines.push(`## Changelog - ${headerDate}`)
|
|
136
|
+
}
|
|
137
|
+
lines.push('')
|
|
138
|
+
|
|
139
|
+
const sortedCategories = Object.values(CATEGORIES).sort((a, b) => a.order - b.order)
|
|
140
|
+
|
|
141
|
+
let hasEntries = false
|
|
142
|
+
for (const cat of sortedCategories) {
|
|
143
|
+
const list = sections[cat.key] || []
|
|
144
|
+
if (list.length === 0) continue
|
|
145
|
+
hasEntries = true
|
|
146
|
+
lines.push(`### ${cat.title}`)
|
|
147
|
+
for (const item of list) {
|
|
148
|
+
const scopePart = item.scope ? `**${item.scope}**: ` : ''
|
|
149
|
+
const shaPart = item.sha ? ` (${item.sha})` : ''
|
|
150
|
+
const breakingPrefix = (cat.key === 'breaking' && item.breakingNote) ? `*${item.breakingNote}* — ` : ''
|
|
151
|
+
lines.push(`- ${scopePart}${breakingPrefix}${item.subject}${shaPart}`)
|
|
152
|
+
}
|
|
153
|
+
lines.push('')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!hasEntries) {
|
|
157
|
+
lines.push('*No significant user-facing changes.*')
|
|
158
|
+
lines.push('')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (compareUrl) {
|
|
162
|
+
lines.push(`[Full Diff](${compareUrl})`)
|
|
163
|
+
lines.push('')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n').trim()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** High level changelog generator. */
|
|
170
|
+
export async function generateChangelog(client, {
|
|
171
|
+
owner, repo, base, head = 'HEAD', version, date, commits: customCommits,
|
|
172
|
+
} = {}) {
|
|
173
|
+
let commitList = []
|
|
174
|
+
let compareUrl = ''
|
|
175
|
+
let previousTag = base || ''
|
|
176
|
+
|
|
177
|
+
if (Array.isArray(customCommits)) {
|
|
178
|
+
commitList = customCommits.map((c) => {
|
|
179
|
+
if (typeof c === 'string') return { message: c, sha: '' }
|
|
180
|
+
return { message: c.message || c.commit?.message || '', sha: c.sha || '' }
|
|
181
|
+
})
|
|
182
|
+
} else if (client && owner && repo) {
|
|
183
|
+
if (!previousTag) {
|
|
184
|
+
try {
|
|
185
|
+
const { data: tags } = await client.get(`/repos/${owner}/${repo}/tags`, { query: { per_page: 1 } })
|
|
186
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
187
|
+
previousTag = tags[0].name
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// ignore tag fetch failure
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (previousTag) {
|
|
195
|
+
try {
|
|
196
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/compare/${encodeURIComponent(previousTag)}...${encodeURIComponent(head)}`)
|
|
197
|
+
compareUrl = data?.html_url || ''
|
|
198
|
+
const rawCommits = data?.commits || []
|
|
199
|
+
commitList = rawCommits.map((c) => ({
|
|
200
|
+
message: c.commit?.message || '',
|
|
201
|
+
sha: c.sha || '',
|
|
202
|
+
author: c.author?.login || c.commit?.author?.name || '',
|
|
203
|
+
}))
|
|
204
|
+
} catch {
|
|
205
|
+
commitList = []
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
try {
|
|
209
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/commits`, { query: { per_page: 50, sha: head } })
|
|
210
|
+
const rawCommits = Array.isArray(data) ? data : []
|
|
211
|
+
commitList = rawCommits.map((c) => ({
|
|
212
|
+
message: c.commit?.message || '',
|
|
213
|
+
sha: c.sha || '',
|
|
214
|
+
author: c.author?.login || c.commit?.author?.name || '',
|
|
215
|
+
}))
|
|
216
|
+
} catch {
|
|
217
|
+
commitList = []
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const parsed = commitList.map((c) => parseConventionalCommit(c.message, c.sha))
|
|
223
|
+
const sections = {}
|
|
224
|
+
for (const catKey of Object.keys(CATEGORIES)) {
|
|
225
|
+
sections[catKey] = []
|
|
226
|
+
}
|
|
227
|
+
for (const item of parsed) {
|
|
228
|
+
const key = item.category in sections ? item.category : 'other'
|
|
229
|
+
sections[key].push(item)
|
|
230
|
+
if (item.isBreaking && item.type in sections && item.type !== 'breaking') {
|
|
231
|
+
sections[item.type].push(item)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const recommendedBump = calculateBump(parsed)
|
|
236
|
+
const nextVersion = version || (previousTag ? bumpVersion(previousTag, recommendedBump) : bumpVersion('0.1.0', recommendedBump))
|
|
237
|
+
const markdown = formatChangelog({ version: nextVersion, date, sections, compareUrl })
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
version: nextVersion,
|
|
241
|
+
previousTag: previousTag || null,
|
|
242
|
+
head,
|
|
243
|
+
recommendedBump,
|
|
244
|
+
commitsCount: parsed.length,
|
|
245
|
+
summary: {
|
|
246
|
+
breaking: sections.breaking.length,
|
|
247
|
+
feat: sections.feat.length,
|
|
248
|
+
fix: sections.fix.length,
|
|
249
|
+
perf: sections.perf.length,
|
|
250
|
+
refactor: sections.refactor.length,
|
|
251
|
+
docs: sections.docs.length,
|
|
252
|
+
test: sections.test.length,
|
|
253
|
+
maintenance: sections.maintenance.length,
|
|
254
|
+
other: sections.other.length,
|
|
255
|
+
total: parsed.length,
|
|
256
|
+
},
|
|
257
|
+
sections,
|
|
258
|
+
markdown,
|
|
259
|
+
commits: parsed,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Community health files scaffolding tools.
|
|
2
|
+
// Standardized templates for GoodAndReady open source plugins and repositories.
|
|
3
|
+
|
|
4
|
+
import { pushFiles } from './write-files.js'
|
|
5
|
+
|
|
6
|
+
export const TEMPLATE_KEYS = [
|
|
7
|
+
'bug_report',
|
|
8
|
+
'feature_request',
|
|
9
|
+
'security_advisory',
|
|
10
|
+
'pull_request',
|
|
11
|
+
'security',
|
|
12
|
+
'contributing',
|
|
13
|
+
'code_of_conduct',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/** Generate file mapping for requested community templates. */
|
|
17
|
+
export function getCommunityTemplates({
|
|
18
|
+
projectName = 'Project',
|
|
19
|
+
contactEmail = 'security@goodandready.app',
|
|
20
|
+
defaultBranch = 'main',
|
|
21
|
+
supportVersion = '>= 0.6.0',
|
|
22
|
+
} = {}) {
|
|
23
|
+
const templates = {}
|
|
24
|
+
|
|
25
|
+
templates['.github/ISSUE_TEMPLATE/bug_report.md'] = `---
|
|
26
|
+
name: Bug Report
|
|
27
|
+
about: Create a report to help us improve
|
|
28
|
+
title: 'fix: '
|
|
29
|
+
labels: ['bug']
|
|
30
|
+
assignees: ''
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 1. Context & Description
|
|
34
|
+
<!-- A clear and concise description of what the bug is. -->
|
|
35
|
+
|
|
36
|
+
### 2. Steps to Reproduce
|
|
37
|
+
1. Go to '...'
|
|
38
|
+
2. Click on '....'
|
|
39
|
+
3. See error
|
|
40
|
+
|
|
41
|
+
### 3. Expected Behavior
|
|
42
|
+
<!-- What you expected to happen. -->
|
|
43
|
+
|
|
44
|
+
### 4. Actual Behavior / Logs
|
|
45
|
+
<!-- Screenshots or error logs if applicable. -->
|
|
46
|
+
|
|
47
|
+
### 5. Environment
|
|
48
|
+
- OS: [e.g. Linux / Windows]
|
|
49
|
+
- Node version: [e.g. 22.x]
|
|
50
|
+
- Plugin version: [e.g. ${supportVersion}]
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
templates['.github/ISSUE_TEMPLATE/feature_request.md'] = `---
|
|
54
|
+
name: Feature Request
|
|
55
|
+
about: Suggest an idea or enhancement
|
|
56
|
+
title: 'feat: '
|
|
57
|
+
labels: ['enhancement']
|
|
58
|
+
assignees: ''
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### 1. Problem Statement
|
|
62
|
+
<!-- Is your feature request related to a problem? Please describe. -->
|
|
63
|
+
|
|
64
|
+
### 2. Proposed Solution
|
|
65
|
+
<!-- Describe the solution you'd like. -->
|
|
66
|
+
|
|
67
|
+
### 3. Acceptance Criteria
|
|
68
|
+
- [ ] Functional requirements met
|
|
69
|
+
- [ ] Automated tests added
|
|
70
|
+
- [ ] Documentation updated
|
|
71
|
+
|
|
72
|
+
### 4. Alternatives Considered
|
|
73
|
+
<!-- Any alternatives or workaround you have considered. -->
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
templates['.github/ISSUE_TEMPLATE/security_advisory.md'] = `---
|
|
77
|
+
name: Security Vulnerability
|
|
78
|
+
about: Privately report a security issue
|
|
79
|
+
title: 'sec: '
|
|
80
|
+
labels: ['security']
|
|
81
|
+
assignees: ''
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
> [!IMPORTANT]
|
|
85
|
+
> If this is a critical zero-day security vulnerability, please contact ${contactEmail} directly instead of opening a public issue.
|
|
86
|
+
|
|
87
|
+
### 1. Summary
|
|
88
|
+
### 2. Severity & Impact
|
|
89
|
+
### 3. Steps / Proof of Concept
|
|
90
|
+
### 4. Remediation / Suggested Patch
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
templates['.github/PULL_REQUEST_TEMPLATE.md'] = `## 1. Context & Problem
|
|
94
|
+
<!-- Reference issue or explain the problem solved. -->
|
|
95
|
+
|
|
96
|
+
## 2. Proposed Changes
|
|
97
|
+
<!-- Detailed explanation of architectural and code modifications. -->
|
|
98
|
+
|
|
99
|
+
## 3. Verification & Testing
|
|
100
|
+
- [ ] Unit tests added / updated
|
|
101
|
+
- [ ] All tests passing (\`npm test\`)
|
|
102
|
+
- [ ] No regression on existing invariants
|
|
103
|
+
|
|
104
|
+
## 4. Documentation & Contracts
|
|
105
|
+
- [ ] Documentation updated (README / API docs)
|
|
106
|
+
- [ ] Conventional Commits used for automated release notes
|
|
107
|
+
|
|
108
|
+
## 5. Review Checklist
|
|
109
|
+
- [ ] Code style and lint verified
|
|
110
|
+
- [ ] Zero hardcoded secrets or credentials
|
|
111
|
+
`
|
|
112
|
+
|
|
113
|
+
templates['SECURITY.md'] = `# Security Policy
|
|
114
|
+
|
|
115
|
+
## Supported Versions
|
|
116
|
+
|
|
117
|
+
| Version | Supported |
|
|
118
|
+
| ------- | ------------------ |
|
|
119
|
+
| ${supportVersion} | :white_check_mark: |
|
|
120
|
+
| < 0.6.0 | :x: |
|
|
121
|
+
|
|
122
|
+
## Reporting a Vulnerability
|
|
123
|
+
|
|
124
|
+
Please report security vulnerabilities directly to \`${contactEmail}\`.
|
|
125
|
+
All reports will be acknowledged within 48 hours, and an advisory/patch roadmap provided within 7 days.
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
templates['CONTRIBUTING.md'] = `# Contributing to ${projectName}
|
|
129
|
+
|
|
130
|
+
Thank you for your interest in contributing!
|
|
131
|
+
|
|
132
|
+
## Development Workflow
|
|
133
|
+
1. Fork or branch from \`${defaultBranch}\`.
|
|
134
|
+
2. Install dependencies: \`pnpm install\` or \`npm install\`.
|
|
135
|
+
3. Run tests: \`npm test\`.
|
|
136
|
+
|
|
137
|
+
## Conventional Commits
|
|
138
|
+
We follow the Conventional Commits specification:
|
|
139
|
+
- \`feat:\` new feature (triggers minor version bump)
|
|
140
|
+
- \`fix:\` bug fix (triggers patch bump)
|
|
141
|
+
- \`perf:\` performance improvement
|
|
142
|
+
- \`refactor:\` code refactoring
|
|
143
|
+
- \`docs:\` documentation updates
|
|
144
|
+
- \`chore:\` maintenance and tooling
|
|
145
|
+
- \`BREAKING CHANGE:\` incompatible API change (triggers major bump)
|
|
146
|
+
|
|
147
|
+
## Pull Request Process
|
|
148
|
+
- Fill out the 5-section PR template.
|
|
149
|
+
- Ensure all automated checks pass.
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
templates['CODE_OF_CONDUCT.md'] = `# Contributor Covenant Code of Conduct
|
|
153
|
+
|
|
154
|
+
## Our Pledge
|
|
155
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
156
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
157
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
158
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
159
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
160
|
+
identity and orientation.
|
|
161
|
+
|
|
162
|
+
## Enforcement & Contact
|
|
163
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
164
|
+
reported to the community leadership at \`${contactEmail}\`.
|
|
165
|
+
`
|
|
166
|
+
|
|
167
|
+
return templates
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Map friendly template keys to path patterns */
|
|
171
|
+
const KEY_TO_PATH_PATTERN = {
|
|
172
|
+
bug_report: '.github/ISSUE_TEMPLATE/bug_report.md',
|
|
173
|
+
feature_request: '.github/ISSUE_TEMPLATE/feature_request.md',
|
|
174
|
+
security_advisory: '.github/ISSUE_TEMPLATE/security_advisory.md',
|
|
175
|
+
issue_templates: '.github/ISSUE_TEMPLATE/',
|
|
176
|
+
pull_request: '.github/PULL_REQUEST_TEMPLATE.md',
|
|
177
|
+
pr_template: '.github/PULL_REQUEST_TEMPLATE.md',
|
|
178
|
+
security: 'SECURITY.md',
|
|
179
|
+
contributing: 'CONTRIBUTING.md',
|
|
180
|
+
code_of_conduct: 'CODE_OF_CONDUCT.md',
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Scaffold community files into a repo branch or preview via dryRun. */
|
|
184
|
+
export async function scaffoldCommunityFiles(client, {
|
|
185
|
+
owner, repo, branch, templates = 'all', options = {}, dryRun = false, message,
|
|
186
|
+
} = {}) {
|
|
187
|
+
const allTemplates = getCommunityTemplates({
|
|
188
|
+
projectName: options.projectName || repo || 'Project',
|
|
189
|
+
contactEmail: options.contactEmail || 'security@goodandready.app',
|
|
190
|
+
defaultBranch: branch || 'main',
|
|
191
|
+
supportVersion: options.supportVersion || '>= 0.6.0',
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
let selectedPaths = Object.keys(allTemplates)
|
|
195
|
+
if (Array.isArray(templates) && !templates.includes('all')) {
|
|
196
|
+
const desiredPatterns = templates.map((t) => KEY_TO_PATH_PATTERN[t] || t)
|
|
197
|
+
selectedPaths = selectedPaths.filter((filePath) =>
|
|
198
|
+
desiredPatterns.some((pattern) => filePath === pattern || filePath.startsWith(pattern))
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const files = selectedPaths.map((filePath) => ({
|
|
203
|
+
path: filePath,
|
|
204
|
+
content: allTemplates[filePath],
|
|
205
|
+
}))
|
|
206
|
+
|
|
207
|
+
if (dryRun) {
|
|
208
|
+
return {
|
|
209
|
+
dryRun: true,
|
|
210
|
+
repository: `${owner}/${repo}`,
|
|
211
|
+
branch: branch || 'main',
|
|
212
|
+
count: files.length,
|
|
213
|
+
files,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!client || !owner || !repo) {
|
|
218
|
+
throw new Error('client, owner and repo are required to commit files')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const commitMessage = message || 'chore: scaffold GoodAndReady community health files'
|
|
222
|
+
const pushed = await pushFiles(client, {
|
|
223
|
+
owner,
|
|
224
|
+
repo,
|
|
225
|
+
branch: branch || 'main',
|
|
226
|
+
message: commitMessage,
|
|
227
|
+
files,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
repository: `${owner}/${repo}`,
|
|
233
|
+
branch: pushed.branch,
|
|
234
|
+
commit: pushed.commit,
|
|
235
|
+
count: files.length,
|
|
236
|
+
files: files.map((f) => f.path),
|
|
237
|
+
}
|
|
238
|
+
}
|