@covibes/zeroshot 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -14
- package/cli/commands/providers.js +8 -9
- package/cli/index.js +3032 -2409
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +1 -1
- package/cluster-templates/base-templates/full-workflow.json +72 -188
- package/cluster-templates/base-templates/worker-validator.json +59 -3
- package/cluster-templates/conductor-bootstrap.json +4 -4
- package/lib/docker-config.js +8 -0
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-names.js +2 -1
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +161 -63
- package/package.json +7 -2
- package/scripts/setup-merge-queue.sh +170 -0
- package/src/agent/agent-config.js +135 -82
- package/src/agent/agent-context-builder.js +297 -188
- package/src/agent/agent-hook-executor.js +310 -113
- package/src/agent/agent-lifecycle.js +385 -325
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +824 -565
- package/src/agent/output-extraction.js +41 -24
- package/src/agent/output-reformatter.js +1 -1
- package/src/agent/schema-utils.js +108 -73
- package/src/agent-wrapper.js +10 -1
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +85 -34
- package/src/config-validator.js +922 -657
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +289 -199
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +22 -5
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1107 -811
- package/src/preflight.js +313 -159
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +53 -25
- package/src/providers/anthropic/index.js +72 -2
- package/src/providers/anthropic/output-parser.js +32 -14
- package/src/providers/base-provider.js +70 -0
- package/src/providers/capabilities.js +9 -0
- package/src/providers/google/output-parser.js +47 -38
- package/src/providers/index.js +19 -3
- package/src/providers/openai/cli-builder.js +14 -3
- package/src/providers/openai/index.js +1 -0
- package/src/providers/openai/output-parser.js +44 -30
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +76 -35
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +118 -81
- package/task-lib/claude-recovery.js +61 -24
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +2 -2
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +84 -27
- package/task-lib/scheduler.js +1 -1
- package/task-lib/store.js +467 -168
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +128 -90
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -139
package/lib/settings.js
CHANGED
|
@@ -13,6 +13,15 @@ const {
|
|
|
13
13
|
normalizeProviderSettings,
|
|
14
14
|
} = require('./provider-names');
|
|
15
15
|
|
|
16
|
+
// Lazy-loaded to avoid circular dependency (issue-providers → settings → issue-providers)
|
|
17
|
+
let _issueProviderFns = null;
|
|
18
|
+
function getIssueProviderFns() {
|
|
19
|
+
if (!_issueProviderFns) {
|
|
20
|
+
_issueProviderFns = require('../src/issue-providers');
|
|
21
|
+
}
|
|
22
|
+
return _issueProviderFns;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Get settings file path (dynamically reads env var for testing)
|
|
18
27
|
* Using a getter ensures tests can override the path at runtime
|
|
@@ -24,6 +33,9 @@ function getSettingsFile() {
|
|
|
24
33
|
);
|
|
25
34
|
}
|
|
26
35
|
|
|
36
|
+
// Import provider defaults from separate module to avoid circular dependency
|
|
37
|
+
const { getProviderDefaults, clearProviderDefaultsCache } = require('./provider-defaults');
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* Model hierarchy for cost ceiling validation
|
|
29
41
|
* Higher number = more expensive/capable model
|
|
@@ -80,30 +92,14 @@ function validateModelAgainstMax(requestedModel, maxModel, minModel = null) {
|
|
|
80
92
|
return requestedModel;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
// Default settings
|
|
84
|
-
const
|
|
95
|
+
// Default settings (base - without issue provider settings which are added dynamically)
|
|
96
|
+
const DEFAULT_SETTINGS_BASE = {
|
|
85
97
|
maxModel: 'opus', // Cost ceiling - agents cannot use models above this
|
|
86
98
|
minModel: null, // Cost floor - agents cannot use models below this (null = no minimum)
|
|
87
99
|
defaultProvider: 'claude',
|
|
88
|
-
providerSettings
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
minLevel: 'level1',
|
|
92
|
-
defaultLevel: 'level2',
|
|
93
|
-
levelOverrides: {},
|
|
94
|
-
},
|
|
95
|
-
codex: {
|
|
96
|
-
maxLevel: 'level3',
|
|
97
|
-
minLevel: 'level1',
|
|
98
|
-
defaultLevel: 'level2',
|
|
99
|
-
levelOverrides: {},
|
|
100
|
-
},
|
|
101
|
-
gemini: {
|
|
102
|
-
maxLevel: 'level3',
|
|
103
|
-
minLevel: 'level1',
|
|
104
|
-
defaultLevel: 'level2',
|
|
105
|
-
levelOverrides: {},
|
|
106
|
-
},
|
|
100
|
+
get providerSettings() {
|
|
101
|
+
// Dynamically build from providers on first access
|
|
102
|
+
return getProviderDefaults();
|
|
107
103
|
},
|
|
108
104
|
defaultConfig: 'conductor-bootstrap',
|
|
109
105
|
defaultDocker: false,
|
|
@@ -117,7 +113,7 @@ const DEFAULT_SETTINGS = {
|
|
|
117
113
|
// Example: 'ccr code' for claude-code-router integration
|
|
118
114
|
claudeCommand: 'claude',
|
|
119
115
|
// Docker isolation mounts - preset names or {host, container, readonly?} objects
|
|
120
|
-
// Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini
|
|
116
|
+
// Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini, opencode
|
|
121
117
|
dockerMounts: ['gh', 'git', 'ssh'],
|
|
122
118
|
// Extra env vars to pass to Docker container (in addition to preset-implied ones)
|
|
123
119
|
// Supports: VAR (if set), VAR_* (pattern), VAR=value (forced), VAR= (empty)
|
|
@@ -125,8 +121,57 @@ const DEFAULT_SETTINGS = {
|
|
|
125
121
|
// Container home directory - where $HOME resolves in container paths
|
|
126
122
|
// Default: /home/node (matches zeroshot-cluster-base image)
|
|
127
123
|
dockerContainerHome: '/home/node',
|
|
124
|
+
// Issue provider settings - defaultIssueSource is here, others come from providers
|
|
125
|
+
defaultIssueSource: 'github', // 'github' | 'gitlab' | 'jira' | 'azure-devops'
|
|
128
126
|
};
|
|
129
127
|
|
|
128
|
+
// Cache for merged defaults (base + issue provider settings)
|
|
129
|
+
let _defaultSettingsCache = null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get DEFAULT_SETTINGS with issue provider settings merged in
|
|
133
|
+
* Lazy-loaded to avoid circular dependency at module load time
|
|
134
|
+
*/
|
|
135
|
+
function getDefaultSettings() {
|
|
136
|
+
if (!_defaultSettingsCache) {
|
|
137
|
+
const issueProviderDefaults = getIssueProviderFns().getIssueProviderSettingsDefaults();
|
|
138
|
+
_defaultSettingsCache = {
|
|
139
|
+
...DEFAULT_SETTINGS_BASE,
|
|
140
|
+
...issueProviderDefaults,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return _defaultSettingsCache;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// For backward compatibility, export DEFAULT_SETTINGS as a getter
|
|
147
|
+
const DEFAULT_SETTINGS = new Proxy(DEFAULT_SETTINGS_BASE, {
|
|
148
|
+
get(target, prop) {
|
|
149
|
+
// For known base properties, return from base
|
|
150
|
+
if (prop in target) {
|
|
151
|
+
return target[prop];
|
|
152
|
+
}
|
|
153
|
+
// For issue provider settings, get from merged defaults
|
|
154
|
+
const merged = getDefaultSettings();
|
|
155
|
+
return merged[prop];
|
|
156
|
+
},
|
|
157
|
+
has(target, prop) {
|
|
158
|
+
if (prop in target) return true;
|
|
159
|
+
const merged = getDefaultSettings();
|
|
160
|
+
return prop in merged;
|
|
161
|
+
},
|
|
162
|
+
ownKeys() {
|
|
163
|
+
const merged = getDefaultSettings();
|
|
164
|
+
return Object.keys(merged);
|
|
165
|
+
},
|
|
166
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
167
|
+
const merged = getDefaultSettings();
|
|
168
|
+
if (prop in merged) {
|
|
169
|
+
return { enumerable: true, configurable: true, value: merged[prop] };
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
130
175
|
function mapLegacyModelToLevel(model) {
|
|
131
176
|
switch (model) {
|
|
132
177
|
case 'haiku':
|
|
@@ -141,10 +186,14 @@ function mapLegacyModelToLevel(model) {
|
|
|
141
186
|
}
|
|
142
187
|
|
|
143
188
|
function mergeProviderSettings(current, overrides) {
|
|
144
|
-
|
|
189
|
+
// Ensure current has all providers with their defaults
|
|
190
|
+
const providerDefaults = getProviderDefaults();
|
|
191
|
+
const merged = {};
|
|
192
|
+
|
|
145
193
|
for (const provider of VALID_PROVIDERS) {
|
|
146
194
|
merged[provider] = {
|
|
147
|
-
...
|
|
195
|
+
...(providerDefaults[provider] || {}),
|
|
196
|
+
...(current[provider] || {}),
|
|
148
197
|
...(overrides?.[provider] || {}),
|
|
149
198
|
};
|
|
150
199
|
if (!merged[provider].levelOverrides) {
|
|
@@ -202,7 +251,11 @@ function normalizeLoadedSettings(parsed) {
|
|
|
202
251
|
function loadSettings() {
|
|
203
252
|
const settingsFile = getSettingsFile();
|
|
204
253
|
if (!fs.existsSync(settingsFile)) {
|
|
205
|
-
|
|
254
|
+
// Return a copy with resolved providerSettings
|
|
255
|
+
return {
|
|
256
|
+
...DEFAULT_SETTINGS,
|
|
257
|
+
providerSettings: getProviderDefaults(),
|
|
258
|
+
};
|
|
206
259
|
}
|
|
207
260
|
try {
|
|
208
261
|
const data = fs.readFileSync(settingsFile, 'utf8');
|
|
@@ -210,14 +263,14 @@ function loadSettings() {
|
|
|
210
263
|
const merged = { ...DEFAULT_SETTINGS, ...parsed };
|
|
211
264
|
merged.defaultProvider =
|
|
212
265
|
normalizeProviderName(merged.defaultProvider) || DEFAULT_SETTINGS.defaultProvider;
|
|
213
|
-
merged.providerSettings = mergeProviderSettings(
|
|
214
|
-
DEFAULT_SETTINGS.providerSettings,
|
|
215
|
-
parsed.providerSettings
|
|
216
|
-
);
|
|
266
|
+
merged.providerSettings = mergeProviderSettings(getProviderDefaults(), parsed.providerSettings);
|
|
217
267
|
return applyLegacyModelBounds(merged);
|
|
218
268
|
} catch {
|
|
219
269
|
console.error('Warning: Could not load settings, using defaults');
|
|
220
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
...DEFAULT_SETTINGS,
|
|
272
|
+
providerSettings: getProviderDefaults(),
|
|
273
|
+
};
|
|
221
274
|
}
|
|
222
275
|
}
|
|
223
276
|
|
|
@@ -233,6 +286,51 @@ function saveSettings(settings) {
|
|
|
233
286
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), 'utf8');
|
|
234
287
|
}
|
|
235
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Validate claudeCommand setting
|
|
291
|
+
* @returns {string|null} Error message if invalid, null if valid
|
|
292
|
+
*/
|
|
293
|
+
function validateClaudeCommand(value) {
|
|
294
|
+
if (typeof value !== 'string') {
|
|
295
|
+
return 'claudeCommand must be a string';
|
|
296
|
+
}
|
|
297
|
+
if (value.trim().length === 0) {
|
|
298
|
+
return 'claudeCommand cannot be empty';
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Validate providerSettings structure by delegating to provider implementations
|
|
305
|
+
* @returns {string|null} Error message if invalid, null if valid
|
|
306
|
+
*/
|
|
307
|
+
function validateProviderSettings(value) {
|
|
308
|
+
const normalizedSettings = normalizeProviderSettings(value);
|
|
309
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
310
|
+
return 'providerSettings must be an object';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Lazy require to avoid circular dependency
|
|
314
|
+
const { getProvider } = require('../src/providers');
|
|
315
|
+
|
|
316
|
+
for (const [providerName, settings] of Object.entries(normalizedSettings || {})) {
|
|
317
|
+
if (!VALID_PROVIDERS.includes(providerName)) {
|
|
318
|
+
return `Unknown provider in providerSettings: ${providerName}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Delegate validation to the provider
|
|
322
|
+
try {
|
|
323
|
+
const provider = getProvider(providerName);
|
|
324
|
+
const error = provider.validateSettings(settings);
|
|
325
|
+
if (error) return error;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return `Failed to validate ${providerName} settings: ${err.message}`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
236
334
|
/**
|
|
237
335
|
* Validate a setting value
|
|
238
336
|
* @returns {string|null} Error message if invalid, null if valid
|
|
@@ -255,12 +353,7 @@ function validateSetting(key, value) {
|
|
|
255
353
|
}
|
|
256
354
|
|
|
257
355
|
if (key === 'claudeCommand') {
|
|
258
|
-
|
|
259
|
-
return 'claudeCommand must be a string';
|
|
260
|
-
}
|
|
261
|
-
if (value.trim().length === 0) {
|
|
262
|
-
return 'claudeCommand cannot be empty';
|
|
263
|
-
}
|
|
356
|
+
return validateClaudeCommand(value);
|
|
264
357
|
}
|
|
265
358
|
|
|
266
359
|
if (key === 'defaultProvider') {
|
|
@@ -271,33 +364,7 @@ function validateSetting(key, value) {
|
|
|
271
364
|
}
|
|
272
365
|
|
|
273
366
|
if (key === 'providerSettings') {
|
|
274
|
-
|
|
275
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
276
|
-
return 'providerSettings must be an object';
|
|
277
|
-
}
|
|
278
|
-
for (const [provider, settings] of Object.entries(normalizedSettings || {})) {
|
|
279
|
-
if (!VALID_PROVIDERS.includes(provider)) {
|
|
280
|
-
return `Unknown provider in providerSettings: ${provider}`;
|
|
281
|
-
}
|
|
282
|
-
if (typeof settings !== 'object' || settings === null) {
|
|
283
|
-
return `providerSettings.${provider} must be an object`;
|
|
284
|
-
}
|
|
285
|
-
if (settings.maxLevel && !LEVEL_RANKS[settings.maxLevel]) {
|
|
286
|
-
return `Invalid maxLevel for ${provider}: ${settings.maxLevel}`;
|
|
287
|
-
}
|
|
288
|
-
if (settings.minLevel && !LEVEL_RANKS[settings.minLevel]) {
|
|
289
|
-
return `Invalid minLevel for ${provider}: ${settings.minLevel}`;
|
|
290
|
-
}
|
|
291
|
-
if (settings.defaultLevel && !LEVEL_RANKS[settings.defaultLevel]) {
|
|
292
|
-
return `Invalid defaultLevel for ${provider}: ${settings.defaultLevel}`;
|
|
293
|
-
}
|
|
294
|
-
if (
|
|
295
|
-
settings.levelOverrides &&
|
|
296
|
-
(typeof settings.levelOverrides !== 'object' || Array.isArray(settings.levelOverrides))
|
|
297
|
-
) {
|
|
298
|
-
return `levelOverrides for ${provider} must be an object`;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
367
|
+
return validateProviderSettings(value);
|
|
301
368
|
}
|
|
302
369
|
|
|
303
370
|
if (key === 'dockerMounts') {
|
|
@@ -308,9 +375,38 @@ function validateSetting(key, value) {
|
|
|
308
375
|
return validateEnvPassthrough(value);
|
|
309
376
|
}
|
|
310
377
|
|
|
378
|
+
// Issue source settings validation (grouped for maintainability)
|
|
379
|
+
const issueSourceError = validateIssueSourceSetting(key, value);
|
|
380
|
+
if (issueSourceError !== undefined) {
|
|
381
|
+
return issueSourceError;
|
|
382
|
+
}
|
|
383
|
+
|
|
311
384
|
return null;
|
|
312
385
|
}
|
|
313
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Validate issue source related settings
|
|
389
|
+
* Delegates to issue providers for provider-specific settings
|
|
390
|
+
* @param {string} key - Setting key
|
|
391
|
+
* @param {any} value - Setting value
|
|
392
|
+
* @returns {string|null|undefined} Error message, null if valid, undefined if not an issue source setting
|
|
393
|
+
*/
|
|
394
|
+
function validateIssueSourceSetting(key, value) {
|
|
395
|
+
// defaultIssueSource is handled here (not provider-specific)
|
|
396
|
+
if (key === 'defaultIssueSource') {
|
|
397
|
+
const { listProviders } = getIssueProviderFns();
|
|
398
|
+
const validSources = listProviders();
|
|
399
|
+
if (!validSources.includes(value)) {
|
|
400
|
+
return `Invalid issue source: ${value}. Valid: ${validSources.join(', ')}`;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Delegate to issue providers for provider-specific settings
|
|
406
|
+
const { validateIssueProviderSetting } = getIssueProviderFns();
|
|
407
|
+
return validateIssueProviderSetting(key, value);
|
|
408
|
+
}
|
|
409
|
+
|
|
314
410
|
/**
|
|
315
411
|
* Coerce value to correct type based on default value type
|
|
316
412
|
*/
|
|
@@ -398,6 +494,8 @@ module.exports = {
|
|
|
398
494
|
MODEL_HIERARCHY,
|
|
399
495
|
VALID_MODELS,
|
|
400
496
|
validateModelAgainstMax,
|
|
497
|
+
// Provider defaults exports
|
|
498
|
+
clearProviderDefaultsCache,
|
|
401
499
|
// Backward compatibility: SETTINGS_FILE as getter (reads env var dynamically)
|
|
402
500
|
get SETTINGS_FILE() {
|
|
403
501
|
return getSettingsFile();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@covibes/zeroshot",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
|
|
5
5
|
"main": "src/orchestrator.js",
|
|
6
6
|
"bin": {
|
|
@@ -118,6 +118,8 @@
|
|
|
118
118
|
"depcheck": "^1.4.7",
|
|
119
119
|
"eslint": "^9.39.1",
|
|
120
120
|
"eslint-config-prettier": "^10.1.8",
|
|
121
|
+
"eslint-plugin-security": "^3.0.1",
|
|
122
|
+
"eslint-plugin-sonarjs": "^3.0.5",
|
|
121
123
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
122
124
|
"husky": "^9.1.7",
|
|
123
125
|
"jscpd": "^3.5.10",
|
|
@@ -131,7 +133,10 @@
|
|
|
131
133
|
"unimported": "^1.31.0"
|
|
132
134
|
},
|
|
133
135
|
"overrides": {
|
|
134
|
-
"xml2js": "^0.5.0"
|
|
136
|
+
"xml2js": "^0.5.0",
|
|
137
|
+
"diff": ">=8.0.3",
|
|
138
|
+
"tar": ">=7.5.3",
|
|
139
|
+
"undici": ">=7.18.2"
|
|
135
140
|
},
|
|
136
141
|
"lint-staged": {
|
|
137
142
|
"*.{js,mjs,cjs}": [
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Setup GitHub merge queue and branch protection for zeroshot
|
|
3
|
+
# Run once after creating the repo or to update settings
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
REPO="covibes/zeroshot"
|
|
8
|
+
|
|
9
|
+
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
|
10
|
+
echo "║ Setting up merge queue for $REPO ║"
|
|
11
|
+
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
|
12
|
+
echo ""
|
|
13
|
+
|
|
14
|
+
# Check gh is authenticated
|
|
15
|
+
if ! gh auth status &>/dev/null; then
|
|
16
|
+
echo "❌ ERROR: Not authenticated with GitHub CLI"
|
|
17
|
+
echo " Run: gh auth login"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check we have admin access
|
|
22
|
+
if ! gh api "repos/$REPO" --jq '.permissions.admin' | grep -q true; then
|
|
23
|
+
echo "❌ ERROR: You need admin access to $REPO"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
echo "✓ Authenticated with admin access"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Configure 'dev' branch protection (merge target)
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
echo "→ Configuring 'dev' branch protection..."
|
|
35
|
+
|
|
36
|
+
gh api --method PUT "repos/$REPO/branches/dev/protection" \
|
|
37
|
+
--input - <<EOF
|
|
38
|
+
{
|
|
39
|
+
"required_status_checks": {
|
|
40
|
+
"strict": true,
|
|
41
|
+
"contexts": ["check", "install-matrix (ubuntu-latest, 20)", "install-matrix (macos-latest, 20)"]
|
|
42
|
+
},
|
|
43
|
+
"enforce_admins": false,
|
|
44
|
+
"required_pull_request_reviews": null,
|
|
45
|
+
"restrictions": null,
|
|
46
|
+
"allow_force_pushes": false,
|
|
47
|
+
"allow_deletions": false,
|
|
48
|
+
"required_linear_history": true,
|
|
49
|
+
"required_conversation_resolution": false
|
|
50
|
+
}
|
|
51
|
+
EOF
|
|
52
|
+
|
|
53
|
+
echo "✓ 'dev' branch protection configured"
|
|
54
|
+
|
|
55
|
+
# Enable merge queue for dev branch
|
|
56
|
+
echo "→ Enabling merge queue for 'dev' branch..."
|
|
57
|
+
|
|
58
|
+
# Note: Merge queue requires GitHub Enterprise or public repos with Actions
|
|
59
|
+
# Using the ruleset API which supports merge queue
|
|
60
|
+
gh api --method POST "repos/$REPO/rulesets" \
|
|
61
|
+
--input - <<EOF 2>/dev/null || echo " (ruleset may already exist)"
|
|
62
|
+
{
|
|
63
|
+
"name": "dev-merge-queue",
|
|
64
|
+
"target": "branch",
|
|
65
|
+
"enforcement": "active",
|
|
66
|
+
"conditions": {
|
|
67
|
+
"ref_name": {
|
|
68
|
+
"include": ["refs/heads/dev"],
|
|
69
|
+
"exclude": []
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"rules": [
|
|
73
|
+
{
|
|
74
|
+
"type": "merge_queue",
|
|
75
|
+
"parameters": {
|
|
76
|
+
"check_response_timeout_minutes": 30,
|
|
77
|
+
"grouping_strategy": "ALLGREEN",
|
|
78
|
+
"max_entries_to_build": 5,
|
|
79
|
+
"max_entries_to_merge": 5,
|
|
80
|
+
"merge_method": "SQUASH",
|
|
81
|
+
"min_entries_to_merge": 1,
|
|
82
|
+
"min_entries_to_merge_wait_minutes": 1
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
EOF
|
|
88
|
+
|
|
89
|
+
echo "✓ Merge queue enabled for 'dev'"
|
|
90
|
+
|
|
91
|
+
# ============================================================================
|
|
92
|
+
# Configure 'main' branch protection (release branch)
|
|
93
|
+
# ============================================================================
|
|
94
|
+
|
|
95
|
+
echo "→ Configuring 'main' branch protection..."
|
|
96
|
+
|
|
97
|
+
gh api --method PUT "repos/$REPO/branches/main/protection" \
|
|
98
|
+
--input - <<EOF
|
|
99
|
+
{
|
|
100
|
+
"required_status_checks": {
|
|
101
|
+
"strict": true,
|
|
102
|
+
"contexts": ["check", "install-matrix (ubuntu-latest, 20)", "install-matrix (macos-latest, 20)"]
|
|
103
|
+
},
|
|
104
|
+
"enforce_admins": false,
|
|
105
|
+
"required_pull_request_reviews": {
|
|
106
|
+
"required_approving_review_count": 1,
|
|
107
|
+
"dismiss_stale_reviews": true,
|
|
108
|
+
"require_code_owner_reviews": false
|
|
109
|
+
},
|
|
110
|
+
"restrictions": null,
|
|
111
|
+
"allow_force_pushes": false,
|
|
112
|
+
"allow_deletions": false,
|
|
113
|
+
"required_linear_history": true,
|
|
114
|
+
"required_conversation_resolution": false
|
|
115
|
+
}
|
|
116
|
+
EOF
|
|
117
|
+
|
|
118
|
+
echo "✓ 'main' branch protection configured"
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# Configure repository settings
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
echo "→ Configuring repository settings..."
|
|
125
|
+
|
|
126
|
+
gh api --method PATCH "repos/$REPO" \
|
|
127
|
+
--input - <<EOF
|
|
128
|
+
{
|
|
129
|
+
"allow_squash_merge": true,
|
|
130
|
+
"allow_merge_commit": false,
|
|
131
|
+
"allow_rebase_merge": false,
|
|
132
|
+
"squash_merge_commit_title": "PR_TITLE",
|
|
133
|
+
"squash_merge_commit_message": "PR_BODY",
|
|
134
|
+
"delete_branch_on_merge": true,
|
|
135
|
+
"allow_auto_merge": true
|
|
136
|
+
}
|
|
137
|
+
EOF
|
|
138
|
+
|
|
139
|
+
echo "✓ Repository settings configured"
|
|
140
|
+
|
|
141
|
+
# ============================================================================
|
|
142
|
+
# Summary
|
|
143
|
+
# ============================================================================
|
|
144
|
+
|
|
145
|
+
echo ""
|
|
146
|
+
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
|
147
|
+
echo "║ ✓ Merge queue setup complete! ║"
|
|
148
|
+
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
|
149
|
+
echo ""
|
|
150
|
+
echo "Workflow:"
|
|
151
|
+
echo " feature-branch (local)"
|
|
152
|
+
echo " ↓"
|
|
153
|
+
echo " pre-push hook → lint + typecheck (~5s)"
|
|
154
|
+
echo " ↓"
|
|
155
|
+
echo " push to origin/feature-branch"
|
|
156
|
+
echo " ↓"
|
|
157
|
+
echo " gh pr create --base dev"
|
|
158
|
+
echo " ↓"
|
|
159
|
+
echo " CI runs tests on PR branch"
|
|
160
|
+
echo " ↓"
|
|
161
|
+
echo " gh pr merge --auto --squash → enters merge queue"
|
|
162
|
+
echo " ↓"
|
|
163
|
+
echo " Queue rebases PR on latest dev + runs CI again"
|
|
164
|
+
echo " ↓"
|
|
165
|
+
echo " Merge to dev (only if CI passes on rebased code)"
|
|
166
|
+
echo ""
|
|
167
|
+
echo "Release workflow:"
|
|
168
|
+
echo " gh pr create --base main --head dev --title \"Release\""
|
|
169
|
+
echo " → CI passes → merge → semantic-release publishes"
|
|
170
|
+
echo ""
|