@uxmaltech/collab-cli 0.1.9 → 0.1.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/README.md +26 -7
- package/dist/commands/end.js +265 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.js +91 -54
- package/dist/lib/executor.js +1 -0
- package/dist/lib/github-api.js +375 -0
- package/dist/stages/github-setup.js +117 -0
- package/dist/templates/ci/canon-sync-trigger.js +37 -0
- package/dist/templates/ci/guard-main-pr.js +24 -0
- package/dist/templates/ci/index.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ collab init --resume # resume from last failed stage
|
|
|
97
97
|
| **Description** | Agents read `.md` files directly | Agents query NebulaGraph + Qdrant via MCP |
|
|
98
98
|
| **Docker** | Not required | Required (Qdrant, NebulaGraph, MCP server) |
|
|
99
99
|
| **MCP** | No | Yes — endpoint `http://127.0.0.1:7337/mcp` |
|
|
100
|
-
| **Wizard stages** | 8 |
|
|
100
|
+
| **Wizard stages** | 8 | 15 |
|
|
101
101
|
| **Use case** | Small projects, no Docker, quick start | Multi-repo ecosystems, large canons |
|
|
102
102
|
|
|
103
103
|
**Transition heuristic:** Consider indexed mode when the canon exceeds ~50,000 tokens (~375 files).
|
|
@@ -114,6 +114,7 @@ collab init --resume # resume from last failed stage
|
|
|
114
114
|
| `collab up` | Full startup pipeline (infra → MCP) |
|
|
115
115
|
| `collab seed` | Preflight check for infrastructure before seeding |
|
|
116
116
|
| `collab doctor` | System diagnostics: config, health, and versions |
|
|
117
|
+
| `collab end` | Finalize work: create PR with governance references and canon sync |
|
|
117
118
|
| `collab update-canons` | Download/update canon from GitHub |
|
|
118
119
|
|
|
119
120
|
## Global options
|
|
@@ -152,27 +153,29 @@ collab init --resume # resume from last failed stage
|
|
|
152
153
|
7. CI setup (GitHub Actions templates)
|
|
153
154
|
8. Agent skills setup (skills and prompts registration)
|
|
154
155
|
|
|
155
|
-
### Indexed (
|
|
156
|
+
### Indexed (15 stages)
|
|
156
157
|
|
|
157
158
|
**Phase A — Local setup (stages 1-8):** Same as file-only, but repo analysis uses AI.
|
|
158
159
|
|
|
159
|
-
**Phase B — Infrastructure (stages 9-
|
|
160
|
+
**Phase B — Infrastructure (stages 9-12):**
|
|
160
161
|
|
|
161
162
|
9. Compose generation (docker-compose.yml or split files)
|
|
162
163
|
10. Infra startup (Qdrant + NebulaGraph via Docker)
|
|
163
164
|
11. MCP startup (MCP service + health checks)
|
|
165
|
+
12. GitHub setup (branch model, protections, CI workflows)
|
|
164
166
|
|
|
165
|
-
**Phase C — Ingestion (stages
|
|
167
|
+
**Phase C — Ingestion (stages 13-15):**
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
13. MCP client config (provider snippets)
|
|
170
|
+
14. Graph seeding (initialize graph with architecture data)
|
|
171
|
+
15. Canon ingest (ingest collab-architecture into Qdrant/Nebula)
|
|
170
172
|
|
|
171
173
|
**Useful flags:**
|
|
172
174
|
- `--resume` — resume from last incomplete stage
|
|
173
175
|
- `--force` — overwrite existing config
|
|
174
176
|
- `--skip-analysis` — skip code analysis
|
|
175
177
|
- `--skip-ci` — skip CI generation
|
|
178
|
+
- `--skip-github-setup` — skip GitHub branch model and workflow configuration
|
|
176
179
|
- `--providers codex,claude` — specify providers
|
|
177
180
|
|
|
178
181
|
## Workspace mode
|
|
@@ -185,6 +188,22 @@ collab init --repos repo-a,repo-b,repo-c
|
|
|
185
188
|
|
|
186
189
|
When run from a directory containing multiple repos, the wizard presents repository selection interactively.
|
|
187
190
|
|
|
191
|
+
## Finalizing work (`collab end`)
|
|
192
|
+
|
|
193
|
+
Create a pull request with governance references and optional canon sync:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
collab end # create PR from current branch to development
|
|
197
|
+
collab end --dry-run # preview PR without creating it
|
|
198
|
+
collab end --title "feat: add login" # override PR title
|
|
199
|
+
collab end --base development # specify target branch (default: development)
|
|
200
|
+
collab end --skip-canon-sync # skip canon sync PR generation
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Context detection:** Automatically parses issue numbers from branch names (e.g., `feature/42-add-login` links to issue #42). In indexed mode, the PR body includes GOV-R-001 phase checklist and governance references.
|
|
204
|
+
|
|
205
|
+
**Canon sync (indexed mode):** When architecture changes are detected (`docs/architecture/`), a separate PR is created in the business-canon repo to complete Phase 5 (Canon Sync) of GOV-R-001.
|
|
206
|
+
|
|
188
207
|
## Local development
|
|
189
208
|
|
|
190
209
|
| Script | Description |
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseIssueFromBranch = parseIssueFromBranch;
|
|
7
|
+
exports.registerEndCommand = registerEndCommand;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const command_context_1 = require("../lib/command-context");
|
|
12
|
+
const errors_1 = require("../lib/errors");
|
|
13
|
+
const github_api_1 = require("../lib/github-api");
|
|
14
|
+
// ────────────────────────────────────────────────────────────────
|
|
15
|
+
// Context detection helpers
|
|
16
|
+
// ────────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Parses an issue number from a branch name following the convention:
|
|
19
|
+
* `feature/42-add-login`, `fix/88-align-flow`, `refactor/10-cleanup`, etc.
|
|
20
|
+
*/
|
|
21
|
+
function parseIssueFromBranch(branch) {
|
|
22
|
+
const match = branch.match(/^(?:feature|fix|refactor|chore|docs|test)\/(\d+)/);
|
|
23
|
+
return match ? parseInt(match[1], 10) : null;
|
|
24
|
+
}
|
|
25
|
+
function getCurrentBranch(cwd) {
|
|
26
|
+
return (0, node_child_process_1.execFileSync)('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
27
|
+
cwd,
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
}).trim();
|
|
31
|
+
}
|
|
32
|
+
function getCommitLog(cwd, base) {
|
|
33
|
+
try {
|
|
34
|
+
return (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `${base}..HEAD`], {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
|
+
}).trim();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function hasGhCli() {
|
|
45
|
+
try {
|
|
46
|
+
(0, node_child_process_1.execFileSync)('gh', ['--version'], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ────────────────────────────────────────────────────────────────
|
|
57
|
+
// PR body generation
|
|
58
|
+
// ────────────────────────────────────────────────────────────────
|
|
59
|
+
function buildPrBody(opts) {
|
|
60
|
+
const lines = ['## Summary', ''];
|
|
61
|
+
if (opts.issueNumber) {
|
|
62
|
+
lines.push(`Resolves #${opts.issueNumber}`, '');
|
|
63
|
+
}
|
|
64
|
+
if (opts.isIndexed) {
|
|
65
|
+
lines.push('## Governance', '');
|
|
66
|
+
if (opts.issueNumber) {
|
|
67
|
+
lines.push(`- **Issue**: #${opts.issueNumber}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push('- **Phase**: Implementation (GOV-R-002)', '');
|
|
70
|
+
lines.push('## GOV-R-001 Phase Checklist', '');
|
|
71
|
+
lines.push('- [x] Phase 1: Epic Definition');
|
|
72
|
+
lines.push('- [x] Phase 2: User Story Decomposition');
|
|
73
|
+
lines.push('- [x] Phase 3: Sub-issue Assignment');
|
|
74
|
+
lines.push('- [x] Phase 4: Implementation');
|
|
75
|
+
lines.push('- [ ] Phase 5: Canon Sync');
|
|
76
|
+
lines.push('');
|
|
77
|
+
}
|
|
78
|
+
if (opts.commitLog) {
|
|
79
|
+
lines.push('## Changes', '', '```', opts.commitLog, '```', '');
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
// ────────────────────────────────────────────────────────────────
|
|
84
|
+
// Canon sync PR generation (#94)
|
|
85
|
+
// ────────────────────────────────────────────────────────────────
|
|
86
|
+
function createCanonSyncPr(opts) {
|
|
87
|
+
const { cwd, canonSlug, repoSlug, issueNumber, dryRun, logger } = opts;
|
|
88
|
+
// Check if there are architecture changes
|
|
89
|
+
const archDir = node_path_1.default.join(cwd, 'docs', 'architecture');
|
|
90
|
+
if (!node_fs_1.default.existsSync(archDir)) {
|
|
91
|
+
logger.info('No docs/architecture directory found; skipping canon sync PR.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Check for changes in architecture dir relative to base
|
|
95
|
+
let archChanges;
|
|
96
|
+
try {
|
|
97
|
+
archChanges = (0, node_child_process_1.execFileSync)('git', ['diff', '--name-only', 'development..HEAD', '--', 'docs/architecture/'], {
|
|
98
|
+
cwd,
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
101
|
+
}).trim();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
archChanges = '';
|
|
105
|
+
}
|
|
106
|
+
if (!archChanges) {
|
|
107
|
+
logger.info('No architecture changes detected; skipping canon sync PR.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const changedFiles = archChanges.split('\n').filter(Boolean);
|
|
111
|
+
const syncBranch = `canon-sync/${repoSlug.split('/')[1]}${issueNumber ? `-${issueNumber}` : ''}`;
|
|
112
|
+
const body = [
|
|
113
|
+
'## Canon Sync',
|
|
114
|
+
'',
|
|
115
|
+
`Source: ${repoSlug}${issueNumber ? `#${issueNumber}` : ''}`,
|
|
116
|
+
'',
|
|
117
|
+
'### Updated files',
|
|
118
|
+
'',
|
|
119
|
+
...changedFiles.map((f) => `- \`${f}\``),
|
|
120
|
+
'',
|
|
121
|
+
'### Governance',
|
|
122
|
+
'',
|
|
123
|
+
`This PR completes Phase 5 (Canon Sync) of GOV-R-001.`,
|
|
124
|
+
'',
|
|
125
|
+
].join('\n');
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
logger.info(`[dry-run] Would create canon sync PR in ${canonSlug}:`);
|
|
128
|
+
logger.info(` Branch: ${syncBranch}`);
|
|
129
|
+
logger.info(` Changed files: ${changedFiles.length}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Create canon sync PR via gh CLI
|
|
133
|
+
try {
|
|
134
|
+
(0, node_child_process_1.execFileSync)('gh', [
|
|
135
|
+
'pr', 'create',
|
|
136
|
+
'-R', canonSlug,
|
|
137
|
+
'--base', 'development',
|
|
138
|
+
'--head', syncBranch,
|
|
139
|
+
'--title', `Canon sync — ${repoSlug.split('/')[1]}${issueNumber ? ` #${issueNumber}` : ''}`,
|
|
140
|
+
'--body', body,
|
|
141
|
+
], {
|
|
142
|
+
cwd,
|
|
143
|
+
encoding: 'utf8',
|
|
144
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
145
|
+
});
|
|
146
|
+
logger.info(`Canon sync PR created in ${canonSlug}.`);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
logger.warn(`Could not create canon sync PR in ${canonSlug}.\n` +
|
|
150
|
+
`Create it manually with: gh pr create -R ${canonSlug} --base development`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ────────────────────────────────────────────────────────────────
|
|
154
|
+
// Command registration
|
|
155
|
+
// ────────────────────────────────────────────────────────────────
|
|
156
|
+
function registerEndCommand(program) {
|
|
157
|
+
program
|
|
158
|
+
.command('end')
|
|
159
|
+
.description('Finalize current work: create PR with governance references')
|
|
160
|
+
.option('--dry-run', 'Show what would be done without executing')
|
|
161
|
+
.option('--skip-canon-sync', 'Skip canon sync PR generation')
|
|
162
|
+
.option('--title <title>', 'Override PR title')
|
|
163
|
+
.option('--base <branch>', 'Target branch (default: development)')
|
|
164
|
+
.addHelpText('after', `
|
|
165
|
+
Examples:
|
|
166
|
+
collab end
|
|
167
|
+
collab end --dry-run
|
|
168
|
+
collab end --title "feat: add login page" --base development
|
|
169
|
+
collab end --skip-canon-sync
|
|
170
|
+
`)
|
|
171
|
+
.action((options, command) => {
|
|
172
|
+
const context = (0, command_context_1.createCommandContext)(command);
|
|
173
|
+
const cwd = context.config.workspaceDir;
|
|
174
|
+
const base = options.base ?? 'development';
|
|
175
|
+
// Validate: collab workspace exists
|
|
176
|
+
if (!node_fs_1.default.existsSync(context.config.configFile)) {
|
|
177
|
+
throw new errors_1.CliError('Not in a collab workspace. Run collab init first.');
|
|
178
|
+
}
|
|
179
|
+
// Validate: gh CLI available
|
|
180
|
+
if (!hasGhCli()) {
|
|
181
|
+
throw new errors_1.CliError('GitHub CLI (gh) is required for collab end.\n' +
|
|
182
|
+
'Install it: https://cli.github.com/');
|
|
183
|
+
}
|
|
184
|
+
// Detect context
|
|
185
|
+
const branch = getCurrentBranch(cwd);
|
|
186
|
+
if (branch === 'development' || branch === 'main') {
|
|
187
|
+
throw new errors_1.CliError(`Cannot create PR from "${branch}". Switch to a feature branch first.\n` +
|
|
188
|
+
'Example: git checkout -b feature/42-description');
|
|
189
|
+
}
|
|
190
|
+
const commitLog = getCommitLog(cwd, base);
|
|
191
|
+
if (!commitLog) {
|
|
192
|
+
throw new errors_1.CliError(`No commits ahead of "${base}". Nothing to create a PR for.`);
|
|
193
|
+
}
|
|
194
|
+
const issueNumber = parseIssueFromBranch(branch);
|
|
195
|
+
const identity = (0, github_api_1.resolveGitHubOwnerRepo)(cwd);
|
|
196
|
+
const canonSlug = context.config.canons?.business?.repo;
|
|
197
|
+
const isIndexed = context.config.mode === 'indexed';
|
|
198
|
+
// Build PR title
|
|
199
|
+
const prTitle = options.title ?? (issueNumber
|
|
200
|
+
? `${branch.replace(/^(feature|fix|refactor|chore|docs|test)\/\d+-?/, '').replace(/-/g, ' ').trim() || `Resolve #${issueNumber}`}`
|
|
201
|
+
: branch.replace(/^(feature|fix|refactor|chore|docs|test)\//, '').replace(/-/g, ' ').trim());
|
|
202
|
+
// Build PR body
|
|
203
|
+
const prBody = buildPrBody({
|
|
204
|
+
issueNumber,
|
|
205
|
+
commitLog,
|
|
206
|
+
canonSlug,
|
|
207
|
+
isIndexed,
|
|
208
|
+
});
|
|
209
|
+
if (options.dryRun) {
|
|
210
|
+
context.logger.info(`[dry-run] Would create PR:`);
|
|
211
|
+
context.logger.info(` Repo: ${identity?.slug ?? cwd}`);
|
|
212
|
+
context.logger.info(` Branch: ${branch} → ${base}`);
|
|
213
|
+
context.logger.info(` Title: ${prTitle}`);
|
|
214
|
+
if (issueNumber)
|
|
215
|
+
context.logger.info(` Issue: #${issueNumber}`);
|
|
216
|
+
context.logger.info('');
|
|
217
|
+
context.logger.info('PR body:');
|
|
218
|
+
context.logger.info(prBody);
|
|
219
|
+
if (!options.skipCanonSync && isIndexed && canonSlug) {
|
|
220
|
+
createCanonSyncPr({
|
|
221
|
+
cwd,
|
|
222
|
+
canonSlug,
|
|
223
|
+
repoSlug: identity?.slug ?? '',
|
|
224
|
+
issueNumber,
|
|
225
|
+
branch,
|
|
226
|
+
dryRun: true,
|
|
227
|
+
logger: context.logger,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Create implementation PR
|
|
233
|
+
context.logger.info(`Creating PR: ${branch} → ${base}...`);
|
|
234
|
+
let prResult;
|
|
235
|
+
try {
|
|
236
|
+
prResult = (0, node_child_process_1.execFileSync)('gh', [
|
|
237
|
+
'pr', 'create',
|
|
238
|
+
'--base', base,
|
|
239
|
+
'--title', prTitle,
|
|
240
|
+
'--body', prBody,
|
|
241
|
+
], {
|
|
242
|
+
cwd,
|
|
243
|
+
encoding: 'utf8',
|
|
244
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
245
|
+
}).trim();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw new errors_1.CliError(`Failed to create PR. Ensure you are authenticated with gh CLI.\n` +
|
|
249
|
+
`Run: gh auth login`);
|
|
250
|
+
}
|
|
251
|
+
context.logger.info(`PR created: ${prResult}`);
|
|
252
|
+
// Canon sync PR (Phase 5)
|
|
253
|
+
if (!options.skipCanonSync && isIndexed && canonSlug) {
|
|
254
|
+
createCanonSyncPr({
|
|
255
|
+
cwd,
|
|
256
|
+
canonSlug,
|
|
257
|
+
repoSlug: identity?.slug ?? '',
|
|
258
|
+
issueNumber,
|
|
259
|
+
branch,
|
|
260
|
+
dryRun: false,
|
|
261
|
+
logger: context.logger,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
package/dist/commands/index.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.registerCommands = registerCommands;
|
|
|
4
4
|
const canon_1 = require("./canon");
|
|
5
5
|
const compose_1 = require("./compose");
|
|
6
6
|
const doctor_1 = require("./doctor");
|
|
7
|
+
const end_1 = require("./end");
|
|
7
8
|
const infra_1 = require("./infra");
|
|
8
9
|
const init_1 = require("./init");
|
|
9
10
|
const mcp_1 = require("./mcp");
|
|
@@ -14,6 +15,7 @@ const update_canons_1 = require("./update-canons");
|
|
|
14
15
|
const upgrade_1 = require("./upgrade");
|
|
15
16
|
function registerCommands(program) {
|
|
16
17
|
(0, init_1.registerInitCommand)(program);
|
|
18
|
+
(0, end_1.registerEndCommand)(program);
|
|
17
19
|
(0, canon_1.registerCanonCommand)(program);
|
|
18
20
|
(0, compose_1.registerComposeCommand)(program);
|
|
19
21
|
(0, infra_1.registerInfraCommand)(program);
|
package/dist/commands/init.js
CHANGED
|
@@ -19,6 +19,7 @@ const mcp_contract_1 = require("../lib/mcp-contract");
|
|
|
19
19
|
const mode_1 = require("../lib/mode");
|
|
20
20
|
const parsers_1 = require("../lib/parsers");
|
|
21
21
|
const orchestrator_1 = require("../lib/orchestrator");
|
|
22
|
+
const github_api_1 = require("../lib/github-api");
|
|
22
23
|
const github_auth_1 = require("../lib/github-auth");
|
|
23
24
|
const prompt_1 = require("../lib/prompt");
|
|
24
25
|
const preflight_1 = require("../lib/preflight");
|
|
@@ -34,6 +35,7 @@ const repo_analysis_1 = require("../stages/repo-analysis");
|
|
|
34
35
|
const repo_analysis_fileonly_1 = require("../stages/repo-analysis-fileonly");
|
|
35
36
|
const agent_skills_setup_1 = require("../stages/agent-skills-setup");
|
|
36
37
|
const ci_setup_1 = require("../stages/ci-setup");
|
|
38
|
+
const github_setup_1 = require("../stages/github-setup");
|
|
37
39
|
const domain_gen_1 = require("../stages/domain-gen");
|
|
38
40
|
const canon_resolver_1 = require("../lib/canon-resolver");
|
|
39
41
|
const providers_1 = require("../lib/providers");
|
|
@@ -192,7 +194,10 @@ function buildGitHubAuthStage(effectiveConfig, logger, options) {
|
|
|
192
194
|
'Run collab init --resume after fixing GitHub access.',
|
|
193
195
|
],
|
|
194
196
|
run: async () => {
|
|
195
|
-
// Skip
|
|
197
|
+
// Skip GitHub auth when no GitHub canon is configured.
|
|
198
|
+
// In indexed mode, a GitHub canon is always required (enforced
|
|
199
|
+
// by parseBusinessCanonOption), so this check only triggers in
|
|
200
|
+
// file-only mode or when preserving an existing config.
|
|
196
201
|
const canon = effectiveConfig.canons?.business;
|
|
197
202
|
if (!canon || canon.source === 'local') {
|
|
198
203
|
logger.info('No GitHub canon configured; skipping GitHub authorization.');
|
|
@@ -274,7 +279,15 @@ function resolveLocalCanonPath(rawPath) {
|
|
|
274
279
|
}
|
|
275
280
|
return resolved;
|
|
276
281
|
}
|
|
277
|
-
function parseBusinessCanonOption(value) {
|
|
282
|
+
function parseBusinessCanonOption(value, mode = 'file-only') {
|
|
283
|
+
if (mode === 'indexed') {
|
|
284
|
+
if (!value || value === 'none' || value === 'skip') {
|
|
285
|
+
throw new errors_1.CliError('Business canon is required for indexed mode. Use --business-canon owner/repo.');
|
|
286
|
+
}
|
|
287
|
+
if (LOCAL_PATH_RE.test(value)) {
|
|
288
|
+
throw new errors_1.CliError('Local business canon is not supported in indexed mode. Use --business-canon owner/repo (GitHub).');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
278
291
|
if (!value || value === 'none' || value === 'skip') {
|
|
279
292
|
return undefined;
|
|
280
293
|
}
|
|
@@ -305,15 +318,24 @@ function parseBusinessCanonOption(value) {
|
|
|
305
318
|
};
|
|
306
319
|
}
|
|
307
320
|
async function resolveBusinessCanon(options, config, logger) {
|
|
321
|
+
const isIndexed = config.mode === 'indexed';
|
|
308
322
|
// CLI flag takes priority
|
|
309
323
|
if (options.businessCanon) {
|
|
310
|
-
return parseBusinessCanonOption(options.businessCanon);
|
|
324
|
+
return parseBusinessCanonOption(options.businessCanon, config.mode);
|
|
311
325
|
}
|
|
312
326
|
// --yes without --business-canon: mandatory error
|
|
313
327
|
if (options.yes) {
|
|
328
|
+
if (isIndexed) {
|
|
329
|
+
throw new errors_1.CliError('--business-canon owner/repo is required with --yes in indexed mode.');
|
|
330
|
+
}
|
|
314
331
|
throw new errors_1.CliError('--business-canon is required with --yes. Use --business-canon owner/repo, --business-canon /local/path, or --business-canon none.');
|
|
315
332
|
}
|
|
316
|
-
// Interactive:
|
|
333
|
+
// Interactive indexed: go straight to GitHub search (no local/skip options)
|
|
334
|
+
if (isIndexed) {
|
|
335
|
+
logger.info('Indexed mode requires a GitHub business canon.');
|
|
336
|
+
return resolveGitHubBusinessCanon(config, logger);
|
|
337
|
+
}
|
|
338
|
+
// Interactive file-only: choose source
|
|
317
339
|
const source = await (0, prompt_1.promptChoice)('Business canon source:', [
|
|
318
340
|
{ value: 'github', label: 'GitHub repository (search and select)' },
|
|
319
341
|
{ value: 'local', label: 'Local directory' },
|
|
@@ -442,18 +464,26 @@ function parseRepos(value) {
|
|
|
442
464
|
return null;
|
|
443
465
|
return value.split(',').map((r) => r.trim()).filter(Boolean);
|
|
444
466
|
}
|
|
445
|
-
async function resolveWorkspace(workspaceDir, options, logger) {
|
|
467
|
+
async function resolveWorkspace(workspaceDir, options, logger, mode = 'file-only') {
|
|
446
468
|
const name = (0, config_1.deriveWorkspaceName)(workspaceDir);
|
|
469
|
+
const isIndexed = mode === 'indexed';
|
|
447
470
|
// Explicit --repos flag takes priority
|
|
448
471
|
const explicit = parseRepos(options.repos);
|
|
449
472
|
if (explicit && explicit.length > 0) {
|
|
450
|
-
|
|
473
|
+
// In indexed mode, always force multi-repo type
|
|
474
|
+
const type = isIndexed || explicit.length >= 2 ? 'multi-repo' : 'mono-repo';
|
|
451
475
|
logger.info(`Workspace mode: ${explicit.length} repo(s) specified: ${explicit.join(', ')}`);
|
|
452
476
|
return { name, type, repos: explicit };
|
|
453
477
|
}
|
|
454
478
|
// Auto-detect workspace layout
|
|
455
479
|
const layout = (0, config_1.detectWorkspaceLayout)(workspaceDir);
|
|
456
480
|
if (layout) {
|
|
481
|
+
// Indexed mode: reject mono-repo
|
|
482
|
+
if (isIndexed && layout.type === 'mono-repo') {
|
|
483
|
+
throw new errors_1.CliError('Indexed mode requires a multi-repo workspace (business-canon + at least 1 governed repo).\n' +
|
|
484
|
+
'Current directory is detected as mono-repo. ' +
|
|
485
|
+
'Run from a parent directory containing multiple git repositories.');
|
|
486
|
+
}
|
|
457
487
|
if (options.yes) {
|
|
458
488
|
logger.info(`Workspace auto-detected (${layout.type}): ${layout.repos.length} repo(s) found: ${layout.repos.join(', ')}`);
|
|
459
489
|
return { name, type: layout.type, repos: layout.repos };
|
|
@@ -465,11 +495,16 @@ async function resolveWorkspace(workspaceDir, options, logger) {
|
|
|
465
495
|
return null;
|
|
466
496
|
return { name, type: 'multi-repo', repos: selected };
|
|
467
497
|
}
|
|
468
|
-
// mono-repo auto-detected
|
|
498
|
+
// mono-repo auto-detected (file-only only — indexed rejected above)
|
|
469
499
|
logger.info(`Mono-repo workspace detected: ${layout.repos.join(', ')}`);
|
|
470
500
|
return { name, type: 'mono-repo', repos: layout.repos };
|
|
471
501
|
}
|
|
472
502
|
// No repos found
|
|
503
|
+
if (isIndexed) {
|
|
504
|
+
throw new errors_1.CliError('Indexed mode requires a multi-repo workspace with at least 1 governed repo.\n' +
|
|
505
|
+
'No git repositories found in the workspace directory.\n' +
|
|
506
|
+
'Clone your repos from GitHub and re-run.');
|
|
507
|
+
}
|
|
473
508
|
if (options.yes) {
|
|
474
509
|
// Non-interactive with no repos → treat cwd as mono-repo
|
|
475
510
|
logger.info('No repos discovered; initializing as mono-repo workspace.');
|
|
@@ -597,6 +632,7 @@ function buildInfraStages(effectiveConfig, executor, logger, options, composeMod
|
|
|
597
632
|
},
|
|
598
633
|
graph_seed_1.graphSeedStage,
|
|
599
634
|
canon_ingest_1.canonIngestStage,
|
|
635
|
+
github_setup_1.githubSetupStage,
|
|
600
636
|
];
|
|
601
637
|
}
|
|
602
638
|
// ────────────────────────────────────────────────────────────────
|
|
@@ -661,6 +697,7 @@ function buildRemoteInfraStages(effectiveConfig, executor, logger, options, mcpU
|
|
|
661
697
|
},
|
|
662
698
|
graph_seed_1.graphSeedStage,
|
|
663
699
|
canon_ingest_1.canonIngestStage,
|
|
700
|
+
github_setup_1.githubSetupStage,
|
|
664
701
|
];
|
|
665
702
|
}
|
|
666
703
|
// ────────────────────────────────────────────────────────────────
|
|
@@ -680,28 +717,6 @@ function buildFileOnlyPipeline(effectiveConfig, executor, logger, configExistedB
|
|
|
680
717
|
];
|
|
681
718
|
}
|
|
682
719
|
// ────────────────────────────────────────────────────────────────
|
|
683
|
-
// Indexed pipeline (15 stages)
|
|
684
|
-
// ────────────────────────────────────────────────────────────────
|
|
685
|
-
function buildIndexedPipeline(effectiveConfig, executor, logger, configExistedBefore, options, composeMode, infraType = 'local', mcpUrl) {
|
|
686
|
-
const infraStages = infraType === 'remote' && mcpUrl
|
|
687
|
-
? buildRemoteInfraStages(effectiveConfig, executor, logger, options, mcpUrl)
|
|
688
|
-
: buildInfraStages(effectiveConfig, executor, logger, options, composeMode);
|
|
689
|
-
return [
|
|
690
|
-
// Phase A — Local setup (shared with file-only)
|
|
691
|
-
buildPreflightStage(executor, logger, infraType === 'local' ? 'indexed' : undefined),
|
|
692
|
-
buildConfigStage(effectiveConfig, executor, logger, configExistedBefore, options.force),
|
|
693
|
-
buildGitHubAuthStage(effectiveConfig, logger, options),
|
|
694
|
-
assistant_setup_1.assistantSetupStage,
|
|
695
|
-
canon_sync_1.canonSyncStage,
|
|
696
|
-
repo_scaffold_1.repoScaffoldStage,
|
|
697
|
-
repo_analysis_1.repoAnalysisStage,
|
|
698
|
-
ci_setup_1.ciSetupStage,
|
|
699
|
-
agent_skills_setup_1.agentSkillsSetupStage,
|
|
700
|
-
// Phase B — Infrastructure + Phase C — Ingestion
|
|
701
|
-
...infraStages,
|
|
702
|
-
];
|
|
703
|
-
}
|
|
704
|
-
// ────────────────────────────────────────────────────────────────
|
|
705
720
|
// Standalone infra phase (collab init infra)
|
|
706
721
|
// ────────────────────────────────────────────────────────────────
|
|
707
722
|
async function runInfraOnly(context, options) {
|
|
@@ -816,8 +831,22 @@ async function runRepoDomainGeneration(context, options) {
|
|
|
816
831
|
...(0, config_1.defaultCollabConfig)(context.config.workspaceDir),
|
|
817
832
|
...context.config,
|
|
818
833
|
};
|
|
834
|
+
// Resolve mode early so parseBusinessCanonOption gets the correct mode context
|
|
835
|
+
let mode;
|
|
836
|
+
if (options.mode) {
|
|
837
|
+
mode = (0, mode_1.parseMode)(options.mode);
|
|
838
|
+
}
|
|
839
|
+
else if (options.yes) {
|
|
840
|
+
mode = 'file-only';
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
mode = await (0, prompt_1.promptChoice)('Select domain generation mode:', [
|
|
844
|
+
{ value: 'file-only', label: 'file-only (write domain files to local repo only)' },
|
|
845
|
+
{ value: 'indexed', label: 'indexed (write to business canon + ingest into MCP)' },
|
|
846
|
+
], 'file-only');
|
|
847
|
+
}
|
|
819
848
|
// Resolve business canon if passed via flag (but don't require it for file-only)
|
|
820
|
-
const canons = options.businessCanon ? parseBusinessCanonOption(options.businessCanon) : undefined;
|
|
849
|
+
const canons = options.businessCanon ? parseBusinessCanonOption(options.businessCanon, mode) : undefined;
|
|
821
850
|
if (canons) {
|
|
822
851
|
effectiveConfig.canons = canons;
|
|
823
852
|
}
|
|
@@ -831,20 +860,6 @@ async function runRepoDomainGeneration(context, options) {
|
|
|
831
860
|
context.logger.info('GitHub token stored from --github-token flag.');
|
|
832
861
|
}
|
|
833
862
|
}
|
|
834
|
-
// Resolve mode
|
|
835
|
-
let mode;
|
|
836
|
-
if (options.mode) {
|
|
837
|
-
mode = (0, mode_1.parseMode)(options.mode);
|
|
838
|
-
}
|
|
839
|
-
else if (options.yes) {
|
|
840
|
-
mode = 'file-only';
|
|
841
|
-
}
|
|
842
|
-
else {
|
|
843
|
-
mode = await (0, prompt_1.promptChoice)('Select domain generation mode:', [
|
|
844
|
-
{ value: 'file-only', label: 'file-only (write domain files to local repo only)' },
|
|
845
|
-
{ value: 'indexed', label: 'indexed (write to business canon + ingest into MCP)' },
|
|
846
|
-
], 'file-only');
|
|
847
|
-
}
|
|
848
863
|
// Validate prerequisites
|
|
849
864
|
if (mode === 'indexed' && !(0, canon_resolver_1.isBusinessCanonConfigured)(effectiveConfig)) {
|
|
850
865
|
throw new errors_1.CliError('Business canon is required for indexed mode. ' +
|
|
@@ -901,6 +916,7 @@ function registerInitCommand(program) {
|
|
|
901
916
|
.option('--skip-mcp-snippets', 'Skip MCP client config snippet generation')
|
|
902
917
|
.option('--skip-analysis', 'Skip AI-powered repository analysis stage')
|
|
903
918
|
.option('--skip-ci', 'Skip CI workflow generation')
|
|
919
|
+
.option('--skip-github-setup', 'Skip GitHub branch model and workflow configuration')
|
|
904
920
|
.option('--providers <list>', 'Comma-separated AI provider list (codex,claude,gemini,copilot)')
|
|
905
921
|
.option('--business-canon <value>', 'Business canon: owner/repo, /local/path, or "none" to skip')
|
|
906
922
|
.option('--github-token <token>', 'GitHub token for non-interactive mode')
|
|
@@ -964,10 +980,14 @@ Examples:
|
|
|
964
980
|
mcpUrl: preserveExisting ? context.config.mcpUrl : selections.mcpUrl,
|
|
965
981
|
};
|
|
966
982
|
// ── Step 2: Business canon configuration ──────────────────
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
if (
|
|
970
|
-
|
|
983
|
+
// When preserving an existing config (no --force), skip canon
|
|
984
|
+
// resolution — the existing canon config is already merged.
|
|
985
|
+
if (!preserveExisting) {
|
|
986
|
+
context.logger.phaseHeader('collab init', 'Business Canon');
|
|
987
|
+
const canons = await resolveBusinessCanon(options, effectiveConfig, context.logger);
|
|
988
|
+
if (canons) {
|
|
989
|
+
effectiveConfig.canons = canons;
|
|
990
|
+
}
|
|
971
991
|
}
|
|
972
992
|
const stageOptions = {
|
|
973
993
|
yes: options.yes,
|
|
@@ -975,13 +995,14 @@ Examples:
|
|
|
975
995
|
outputDir: options.outputDir,
|
|
976
996
|
skipAnalysis: options.skipAnalysis,
|
|
977
997
|
skipCi: options.skipCi,
|
|
998
|
+
skipGithubSetup: options.skipGithubSetup,
|
|
978
999
|
};
|
|
979
1000
|
// ── Workspace detection ───────────────────────────────────
|
|
980
1001
|
// Prefer persisted workspace config when it exists (unless
|
|
981
1002
|
// --force or explicit --repos override is provided).
|
|
982
1003
|
const ws = !options.force && !options.repos && context.config.workspace
|
|
983
1004
|
? context.config.workspace
|
|
984
|
-
: await resolveWorkspace(context.config.workspaceDir, options, context.logger);
|
|
1005
|
+
: await resolveWorkspace(context.config.workspaceDir, options, context.logger, selections.mode);
|
|
985
1006
|
if (ws) {
|
|
986
1007
|
// ── WORKSPACE MODE ────────────────────────────────────
|
|
987
1008
|
effectiveConfig.workspace = { name: ws.name, type: ws.type, repos: ws.repos };
|
|
@@ -989,7 +1010,6 @@ Examples:
|
|
|
989
1010
|
...effectiveConfig.compose,
|
|
990
1011
|
projectName: `collab-${ws.name}`,
|
|
991
1012
|
};
|
|
992
|
-
const repoConfigs = (0, config_1.resolveRepoConfigs)(effectiveConfig);
|
|
993
1013
|
// Phase W — workspace-level stages
|
|
994
1014
|
context.logger.phaseHeader('Workspace Setup', `${ws.repos.length} repositories (${ws.type})`);
|
|
995
1015
|
const workspaceStages = buildWorkspaceStages(effectiveConfig, context.executor, context.logger, configExistedBefore, options);
|
|
@@ -1002,7 +1022,22 @@ Examples:
|
|
|
1002
1022
|
mode: `${selections.mode} (workspace)`,
|
|
1003
1023
|
stageOptions,
|
|
1004
1024
|
}, workspaceStages);
|
|
1025
|
+
// ── Indexed mode: validate all repos are GitHub repos with access ──
|
|
1026
|
+
if (selections.mode === 'indexed' && context.executor.dryRun) {
|
|
1027
|
+
context.logger.info('[dry-run] Would validate GitHub remotes and token access for workspace repos.');
|
|
1028
|
+
}
|
|
1029
|
+
else if (selections.mode === 'indexed') {
|
|
1030
|
+
context.logger.phaseHeader('Repository Validation', 'GitHub access');
|
|
1031
|
+
const auth = (0, github_auth_1.loadGitHubAuth)(effectiveConfig.collabDir);
|
|
1032
|
+
if (!auth) {
|
|
1033
|
+
throw new errors_1.CliError('GitHub authorization required but token not found after auth stage.');
|
|
1034
|
+
}
|
|
1035
|
+
const validRepos = await (0, github_api_1.validateWorkspaceRepos)(ws.repos, effectiveConfig.workspaceDir, auth.token, context.logger);
|
|
1036
|
+
effectiveConfig.workspace = { ...effectiveConfig.workspace, repos: validRepos };
|
|
1037
|
+
ws.repos = validRepos;
|
|
1038
|
+
}
|
|
1005
1039
|
// Phase R — per-repo stages
|
|
1040
|
+
const repoConfigs = (0, config_1.resolveRepoConfigs)(effectiveConfig);
|
|
1006
1041
|
context.logger.phaseHeader('Repository Analysis', `${selections.mode} mode`);
|
|
1007
1042
|
const perRepoStages = buildPerRepoStages(selections.mode);
|
|
1008
1043
|
for (const [i, rc] of repoConfigs.entries()) {
|
|
@@ -1037,11 +1072,13 @@ Examples:
|
|
|
1037
1072
|
}
|
|
1038
1073
|
}
|
|
1039
1074
|
else {
|
|
1040
|
-
// ── SINGLE-REPO MODE
|
|
1075
|
+
// ── SINGLE-REPO MODE ─────────────────────────────────
|
|
1076
|
+
if (selections.mode === 'indexed') {
|
|
1077
|
+
throw new errors_1.CliError('Indexed mode requires a multi-repo workspace.\n' +
|
|
1078
|
+
'Run from a workspace directory with multiple git repos, or use --repos to specify repos.');
|
|
1079
|
+
}
|
|
1041
1080
|
context.logger.phaseHeader('Project Setup', selections.mode);
|
|
1042
|
-
const stages =
|
|
1043
|
-
? buildFileOnlyPipeline(effectiveConfig, context.executor, context.logger, configExistedBefore, options)
|
|
1044
|
-
: buildIndexedPipeline(effectiveConfig, context.executor, context.logger, configExistedBefore, options, selections.composeMode, selections.infraType, selections.mcpUrl);
|
|
1081
|
+
const stages = buildFileOnlyPipeline(effectiveConfig, context.executor, context.logger, configExistedBefore, options);
|
|
1045
1082
|
await (0, orchestrator_1.runOrchestration)({
|
|
1046
1083
|
workflowId: 'init',
|
|
1047
1084
|
config: effectiveConfig,
|
package/dist/lib/executor.js
CHANGED
|
@@ -38,6 +38,7 @@ class Executor {
|
|
|
38
38
|
const result = (0, node_child_process_1.spawnSync)(commandName, args, {
|
|
39
39
|
cwd: options.cwd ?? this.cwd,
|
|
40
40
|
encoding: 'utf8',
|
|
41
|
+
...(options.input !== undefined ? { input: options.input } : {}),
|
|
41
42
|
});
|
|
42
43
|
if (result.error) {
|
|
43
44
|
const errorCode = result.error.code;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.normalizeGitHubRemote = normalizeGitHubRemote;
|
|
7
|
+
exports.resolveGitHubOwnerRepo = resolveGitHubOwnerRepo;
|
|
8
|
+
exports.verifyGitHubAccess = verifyGitHubAccess;
|
|
9
|
+
exports.validateWorkspaceRepos = validateWorkspaceRepos;
|
|
10
|
+
exports.getRepoInfo = getRepoInfo;
|
|
11
|
+
exports.getBranchRef = getBranchRef;
|
|
12
|
+
exports.createBranch = createBranch;
|
|
13
|
+
exports.setDefaultBranch = setDefaultBranch;
|
|
14
|
+
exports.setBranchProtection = setBranchProtection;
|
|
15
|
+
exports.setMergeStrategy = setMergeStrategy;
|
|
16
|
+
exports.configureRepo = configureRepo;
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const errors_1 = require("./errors");
|
|
20
|
+
const GITHUB_API_VERSION = '2022-11-28';
|
|
21
|
+
const ACCESS_CHECK_TIMEOUT_MS = 10_000;
|
|
22
|
+
// ────────────────────────────────────────────────────────────────
|
|
23
|
+
// Remote URL normalization
|
|
24
|
+
// ────────────────────────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes a git remote URL to a GitHub `owner/repo` slug.
|
|
27
|
+
* Handles HTTPS, SSH (`git@`), and `ssh://` URL formats.
|
|
28
|
+
* Returns `null` if the remote is not a `github.com` URL.
|
|
29
|
+
*/
|
|
30
|
+
function normalizeGitHubRemote(remoteUrl) {
|
|
31
|
+
if (!/github\.com[:/]/i.test(remoteUrl)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const slug = remoteUrl
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/\/+$/, '')
|
|
37
|
+
.replace(/\.git$/, '')
|
|
38
|
+
.replace(/^https?:\/\/github\.com\//i, '')
|
|
39
|
+
.replace(/^git@github\.com:/i, '')
|
|
40
|
+
.replace(/^ssh:\/\/git@github\.com\//i, '');
|
|
41
|
+
const parts = slug.split('/').filter(Boolean);
|
|
42
|
+
if (parts.length < 2) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return `${parts[0]}/${parts[1]}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads the `origin` remote URL of a local git repo and extracts
|
|
49
|
+
* the GitHub owner/repo identity.
|
|
50
|
+
* Returns `null` if the repo has no git origin, no remote, or a non-GitHub remote.
|
|
51
|
+
*/
|
|
52
|
+
function resolveGitHubOwnerRepo(repoDir) {
|
|
53
|
+
let remoteUrl;
|
|
54
|
+
try {
|
|
55
|
+
remoteUrl = (0, node_child_process_1.execFileSync)('git', ['-C', repoDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const slug = normalizeGitHubRemote(remoteUrl);
|
|
61
|
+
if (!slug) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const [owner, repo] = slug.split('/');
|
|
65
|
+
return { owner, repo, slug };
|
|
66
|
+
}
|
|
67
|
+
// ────────────────────────────────────────────────────────────────
|
|
68
|
+
// GitHub access verification
|
|
69
|
+
// ────────────────────────────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Verifies the GitHub token has read access to the given repo.
|
|
72
|
+
* Returns `true` if `GET /repos/{slug}` returns 200.
|
|
73
|
+
*/
|
|
74
|
+
async function verifyGitHubAccess(slug, token) {
|
|
75
|
+
const url = `https://api.github.com/repos/${slug}`;
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(url, {
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${token}`,
|
|
82
|
+
Accept: 'application/vnd.github+json',
|
|
83
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
84
|
+
},
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
});
|
|
87
|
+
return response.ok;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ────────────────────────────────────────────────────────────────
|
|
97
|
+
// Workspace repo validation
|
|
98
|
+
// ────────────────────────────────────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* Validates that workspace repos have GitHub origin remotes with token access.
|
|
101
|
+
* Returns only the repos that pass. Throws `CliError` if zero repos pass.
|
|
102
|
+
*/
|
|
103
|
+
async function validateWorkspaceRepos(repoNames, workspaceDir, token, logger) {
|
|
104
|
+
const valid = [];
|
|
105
|
+
const excluded = [];
|
|
106
|
+
for (const name of repoNames) {
|
|
107
|
+
const repoDir = node_path_1.default.join(workspaceDir, name);
|
|
108
|
+
const identity = resolveGitHubOwnerRepo(repoDir);
|
|
109
|
+
if (!identity) {
|
|
110
|
+
excluded.push({ name, reason: 'no GitHub remote' });
|
|
111
|
+
logger.warn(`Excluding "${name}": no GitHub origin remote found.`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const hasAccess = await verifyGitHubAccess(identity.slug, token);
|
|
115
|
+
if (!hasAccess) {
|
|
116
|
+
excluded.push({ name, reason: `no access to ${identity.slug}` });
|
|
117
|
+
logger.warn(`Excluding "${name}" (${identity.slug}): GitHub token lacks access.`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
logger.info(`Repo "${name}" (${identity.slug}): GitHub access verified.`);
|
|
121
|
+
valid.push(name);
|
|
122
|
+
}
|
|
123
|
+
if (excluded.length > 0) {
|
|
124
|
+
logger.info(`Excluded ${excluded.length} repo(s): ${excluded.map((e) => `${e.name} (${e.reason})`).join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
if (valid.length === 0) {
|
|
127
|
+
throw new errors_1.CliError('No repos in the workspace have a valid GitHub remote with token access.\n' +
|
|
128
|
+
'Indexed mode requires at least 1 governed GitHub repo in addition to the business canon.\n' +
|
|
129
|
+
'Clone your repos from GitHub and re-run.');
|
|
130
|
+
}
|
|
131
|
+
logger.info(`Found ${valid.length} governed repo(s): ${valid.join(', ')}`);
|
|
132
|
+
return valid;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Fetches repository metadata from the GitHub API.
|
|
136
|
+
*/
|
|
137
|
+
async function getRepoInfo(slug, token) {
|
|
138
|
+
const url = `https://api.github.com/repos/${slug}`;
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${token}`,
|
|
145
|
+
Accept: 'application/vnd.github+json',
|
|
146
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
147
|
+
},
|
|
148
|
+
signal: controller.signal,
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
throw new errors_1.CliError(`GitHub API error ${response.status} for ${slug}: ${response.statusText}`);
|
|
152
|
+
}
|
|
153
|
+
const data = (await response.json());
|
|
154
|
+
return {
|
|
155
|
+
default_branch: data.default_branch,
|
|
156
|
+
allow_merge_commit: data.allow_merge_commit,
|
|
157
|
+
allow_squash_merge: data.allow_squash_merge,
|
|
158
|
+
allow_rebase_merge: data.allow_rebase_merge,
|
|
159
|
+
delete_branch_on_merge: data.delete_branch_on_merge,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ────────────────────────────────────────────────────────────────
|
|
167
|
+
// Branch operations
|
|
168
|
+
// ────────────────────────────────────────────────────────────────
|
|
169
|
+
/**
|
|
170
|
+
* Gets the SHA of a branch ref. Returns `null` if the branch does not exist (404).
|
|
171
|
+
*/
|
|
172
|
+
async function getBranchRef(slug, branch, token) {
|
|
173
|
+
const url = `https://api.github.com/repos/${slug}/git/ref/heads/${branch}`;
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(url, {
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: `Bearer ${token}`,
|
|
180
|
+
Accept: 'application/vnd.github+json',
|
|
181
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
182
|
+
},
|
|
183
|
+
signal: controller.signal,
|
|
184
|
+
});
|
|
185
|
+
if (response.status === 404) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
throw new errors_1.CliError(`GitHub API error ${response.status} checking branch ${branch} for ${slug}`);
|
|
190
|
+
}
|
|
191
|
+
const data = (await response.json());
|
|
192
|
+
return data.object.sha;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Creates a new branch from a given SHA. Idempotent: swallows 422 "Reference already exists".
|
|
200
|
+
*/
|
|
201
|
+
async function createBranch(slug, branch, fromSha, token) {
|
|
202
|
+
const url = `https://api.github.com/repos/${slug}/git/refs`;
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch(url, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: {
|
|
209
|
+
Authorization: `Bearer ${token}`,
|
|
210
|
+
Accept: 'application/vnd.github+json',
|
|
211
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: fromSha }),
|
|
215
|
+
signal: controller.signal,
|
|
216
|
+
});
|
|
217
|
+
// 422 = "Reference already exists" — idempotent, safe to ignore
|
|
218
|
+
if (response.status === 422) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new errors_1.CliError(`GitHub API error ${response.status} creating branch ${branch} for ${slug}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ────────────────────────────────────────────────────────────────
|
|
230
|
+
// Repo configuration
|
|
231
|
+
// ────────────────────────────────────────────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Sets the default branch for a repository.
|
|
234
|
+
* Idempotent: skips if already set.
|
|
235
|
+
*/
|
|
236
|
+
async function setDefaultBranch(slug, branch, token) {
|
|
237
|
+
const url = `https://api.github.com/repos/${slug}`;
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(url, {
|
|
242
|
+
method: 'PATCH',
|
|
243
|
+
headers: {
|
|
244
|
+
Authorization: `Bearer ${token}`,
|
|
245
|
+
Accept: 'application/vnd.github+json',
|
|
246
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
247
|
+
'Content-Type': 'application/json',
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({ default_branch: branch }),
|
|
250
|
+
signal: controller.signal,
|
|
251
|
+
});
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new errors_1.CliError(`GitHub API error ${response.status} setting default branch for ${slug}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Applies branch protection rules. PUT is idempotent by HTTP spec.
|
|
262
|
+
*/
|
|
263
|
+
async function setBranchProtection(slug, branch, token) {
|
|
264
|
+
const url = `https://api.github.com/repos/${slug}/branches/${branch}/protection`;
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
267
|
+
try {
|
|
268
|
+
const response = await fetch(url, {
|
|
269
|
+
method: 'PUT',
|
|
270
|
+
headers: {
|
|
271
|
+
Authorization: `Bearer ${token}`,
|
|
272
|
+
Accept: 'application/vnd.github+json',
|
|
273
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
274
|
+
'Content-Type': 'application/json',
|
|
275
|
+
},
|
|
276
|
+
body: JSON.stringify({
|
|
277
|
+
required_status_checks: null,
|
|
278
|
+
enforce_admins: false,
|
|
279
|
+
required_pull_request_reviews: {
|
|
280
|
+
required_approving_review_count: 1,
|
|
281
|
+
dismiss_stale_reviews: true,
|
|
282
|
+
},
|
|
283
|
+
restrictions: null,
|
|
284
|
+
}),
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
});
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new errors_1.CliError(`GitHub API error ${response.status} setting branch protection on ${branch} for ${slug}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Configures merge strategy: only merge commits, no squash/rebase, delete branch on merge.
|
|
297
|
+
* PATCH is safe to repeat.
|
|
298
|
+
*/
|
|
299
|
+
async function setMergeStrategy(slug, token) {
|
|
300
|
+
const url = `https://api.github.com/repos/${slug}`;
|
|
301
|
+
const controller = new AbortController();
|
|
302
|
+
const timer = setTimeout(() => controller.abort(), ACCESS_CHECK_TIMEOUT_MS);
|
|
303
|
+
try {
|
|
304
|
+
const response = await fetch(url, {
|
|
305
|
+
method: 'PATCH',
|
|
306
|
+
headers: {
|
|
307
|
+
Authorization: `Bearer ${token}`,
|
|
308
|
+
Accept: 'application/vnd.github+json',
|
|
309
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
310
|
+
'Content-Type': 'application/json',
|
|
311
|
+
},
|
|
312
|
+
body: JSON.stringify({
|
|
313
|
+
allow_merge_commit: true,
|
|
314
|
+
allow_squash_merge: false,
|
|
315
|
+
allow_rebase_merge: false,
|
|
316
|
+
delete_branch_on_merge: true,
|
|
317
|
+
}),
|
|
318
|
+
signal: controller.signal,
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
throw new errors_1.CliError(`GitHub API error ${response.status} setting merge strategy for ${slug}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Orchestrates full GitHub configuration for a single repo:
|
|
330
|
+
* branch model → default branch → protection → merge strategy.
|
|
331
|
+
*/
|
|
332
|
+
async function configureRepo(slug, token, logger) {
|
|
333
|
+
logger.info(`Configuring branch model for ${slug}...`);
|
|
334
|
+
// 1. Get current repo info
|
|
335
|
+
const info = await getRepoInfo(slug, token);
|
|
336
|
+
// 2. Ensure both branches exist
|
|
337
|
+
const mainSha = await getBranchRef(slug, 'main', token);
|
|
338
|
+
const devSha = await getBranchRef(slug, 'development', token);
|
|
339
|
+
if (!mainSha && !devSha) {
|
|
340
|
+
// Neither exists — get the current default branch SHA and create both
|
|
341
|
+
const defaultSha = await getBranchRef(slug, info.default_branch, token);
|
|
342
|
+
if (!defaultSha) {
|
|
343
|
+
throw new errors_1.CliError(`Cannot resolve SHA for default branch "${info.default_branch}" of ${slug}`);
|
|
344
|
+
}
|
|
345
|
+
await createBranch(slug, 'main', defaultSha, token);
|
|
346
|
+
logger.info(` Created branch "main" from "${info.default_branch}".`);
|
|
347
|
+
await createBranch(slug, 'development', defaultSha, token);
|
|
348
|
+
logger.info(` Created branch "development" from "${info.default_branch}".`);
|
|
349
|
+
}
|
|
350
|
+
else if (!mainSha && devSha) {
|
|
351
|
+
await createBranch(slug, 'main', devSha, token);
|
|
352
|
+
logger.info(` Created branch "main" from "development".`);
|
|
353
|
+
}
|
|
354
|
+
else if (mainSha && !devSha) {
|
|
355
|
+
await createBranch(slug, 'development', mainSha, token);
|
|
356
|
+
logger.info(` Created branch "development" from "main".`);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
logger.info(` Branches "main" and "development" already exist.`);
|
|
360
|
+
}
|
|
361
|
+
// 3. Set default branch to development
|
|
362
|
+
if (info.default_branch !== 'development') {
|
|
363
|
+
await setDefaultBranch(slug, 'development', token);
|
|
364
|
+
logger.info(` Set default branch to "development" (was "${info.default_branch}").`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
logger.info(` Default branch is already "development".`);
|
|
368
|
+
}
|
|
369
|
+
// 4. Protect main
|
|
370
|
+
await setBranchProtection(slug, 'main', token);
|
|
371
|
+
logger.info(` Applied branch protection on "main" (1 review required).`);
|
|
372
|
+
// 5. Merge strategy
|
|
373
|
+
await setMergeStrategy(slug, token);
|
|
374
|
+
logger.info(` Merge strategy: merge-commit only, delete branch on merge.`);
|
|
375
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.githubSetupStage = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const config_1 = require("../lib/config");
|
|
10
|
+
const github_api_1 = require("../lib/github-api");
|
|
11
|
+
const github_auth_1 = require("../lib/github-auth");
|
|
12
|
+
const errors_1 = require("../lib/errors");
|
|
13
|
+
const guard_main_pr_1 = require("../templates/ci/guard-main-pr");
|
|
14
|
+
const canon_sync_trigger_1 = require("../templates/ci/canon-sync-trigger");
|
|
15
|
+
/**
|
|
16
|
+
* Configures GitHub repos: branch model, protection, merge strategy,
|
|
17
|
+
* guard-main-pr workflow, canon-sync-trigger workflow, and CANON_SYNC_PAT secret.
|
|
18
|
+
*
|
|
19
|
+
* Only runs in indexed mode. Skipped with `--skip-github-setup`.
|
|
20
|
+
*/
|
|
21
|
+
exports.githubSetupStage = {
|
|
22
|
+
id: 'github-setup',
|
|
23
|
+
title: 'Configure GitHub branch model, protections, and automation workflows',
|
|
24
|
+
recovery: [
|
|
25
|
+
'Ensure GitHub token has repo scope.',
|
|
26
|
+
'Run collab init --resume to retry GitHub setup.',
|
|
27
|
+
'Use --skip-github-setup to bypass this stage.',
|
|
28
|
+
],
|
|
29
|
+
run: async (ctx) => {
|
|
30
|
+
if (ctx.options?.skipGithubSetup) {
|
|
31
|
+
ctx.logger.info('Skipping GitHub setup by user choice.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (ctx.config.mode !== 'indexed') {
|
|
35
|
+
ctx.logger.info('GitHub setup is only available in indexed mode; skipping.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (ctx.executor.dryRun) {
|
|
39
|
+
ctx.logger.info('[dry-run] Would configure GitHub branch model, protections, and workflows for workspace repos.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Load GitHub token
|
|
43
|
+
const auth = (0, github_auth_1.loadGitHubAuth)(ctx.config.collabDir);
|
|
44
|
+
if (!auth) {
|
|
45
|
+
throw new errors_1.CliError('GitHub authorization required but token not found.\n' +
|
|
46
|
+
'Run collab init --resume after authenticating with GitHub.');
|
|
47
|
+
}
|
|
48
|
+
const { token } = auth;
|
|
49
|
+
// Resolve business canon slug
|
|
50
|
+
const canonSlug = ctx.config.canons?.business?.repo;
|
|
51
|
+
// Configure governed repos
|
|
52
|
+
const repoConfigs = (0, config_1.resolveRepoConfigs)(ctx.config);
|
|
53
|
+
for (const rc of repoConfigs) {
|
|
54
|
+
const identity = (0, github_api_1.resolveGitHubOwnerRepo)(rc.repoDir);
|
|
55
|
+
if (!identity) {
|
|
56
|
+
ctx.logger.warn(`Skipping "${rc.name}": no GitHub origin remote.`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Branch model + protection + merge strategy
|
|
60
|
+
await (0, github_api_1.configureRepo)(identity.slug, token, ctx.logger);
|
|
61
|
+
// guard-main-pr.yml
|
|
62
|
+
const guardPath = node_path_1.default.join(rc.repoDir, '.github', 'workflows', 'guard-main-pr.yml');
|
|
63
|
+
if (!node_fs_1.default.existsSync(guardPath)) {
|
|
64
|
+
ctx.executor.writeFile(guardPath, guard_main_pr_1.guardMainPrTemplate, {
|
|
65
|
+
description: `write guard-main-pr workflow for ${rc.name}`,
|
|
66
|
+
});
|
|
67
|
+
ctx.logger.info(` Created guard-main-pr.yml for ${rc.name}.`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
ctx.logger.info(` guard-main-pr.yml already exists for ${rc.name}; skipping.`);
|
|
71
|
+
}
|
|
72
|
+
// canon-sync-trigger.yml (only for governed repos, not the canon itself)
|
|
73
|
+
if (canonSlug && identity.slug !== canonSlug) {
|
|
74
|
+
const triggerPath = node_path_1.default.join(rc.repoDir, '.github', 'workflows', 'canon-sync-trigger.yml');
|
|
75
|
+
if (!node_fs_1.default.existsSync(triggerPath)) {
|
|
76
|
+
ctx.executor.writeFile(triggerPath, (0, canon_sync_trigger_1.canonSyncTriggerTemplate)(canonSlug), {
|
|
77
|
+
description: `write canon-sync-trigger workflow for ${rc.name}`,
|
|
78
|
+
});
|
|
79
|
+
ctx.logger.info(` Created canon-sync-trigger.yml for ${rc.name}.`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
ctx.logger.info(` canon-sync-trigger.yml already exists for ${rc.name}; skipping.`);
|
|
83
|
+
}
|
|
84
|
+
// CANON_SYNC_PAT secret via gh CLI (passed via stdin for security)
|
|
85
|
+
try {
|
|
86
|
+
ctx.executor.run('gh', [
|
|
87
|
+
'secret', 'set', 'CANON_SYNC_PAT',
|
|
88
|
+
'-R', identity.slug,
|
|
89
|
+
], { check: true, input: token });
|
|
90
|
+
ctx.logger.info(` Set CANON_SYNC_PAT secret for ${identity.slug}.`);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
ctx.logger.warn(` Could not set CANON_SYNC_PAT for ${identity.slug}.\n` +
|
|
94
|
+
` Set it manually: gh secret set CANON_SYNC_PAT -R ${identity.slug}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Configure business-canon repo (same branch model, but no canon-sync-trigger)
|
|
99
|
+
if (canonSlug) {
|
|
100
|
+
ctx.logger.info(`Configuring business-canon repo: ${canonSlug}...`);
|
|
101
|
+
await (0, github_api_1.configureRepo)(canonSlug, token, ctx.logger);
|
|
102
|
+
// guard-main-pr.yml for the canon repo (write to local clone if available)
|
|
103
|
+
const canonLocalDir = ctx.config.canons?.business?.localDir;
|
|
104
|
+
if (canonLocalDir) {
|
|
105
|
+
const canonRepoDir = node_path_1.default.join(ctx.config.architectureDir, canonLocalDir);
|
|
106
|
+
const guardPath = node_path_1.default.join(canonRepoDir, '.github', 'workflows', 'guard-main-pr.yml');
|
|
107
|
+
if (node_fs_1.default.existsSync(canonRepoDir) && !node_fs_1.default.existsSync(guardPath)) {
|
|
108
|
+
ctx.executor.writeFile(guardPath, guard_main_pr_1.guardMainPrTemplate, {
|
|
109
|
+
description: `write guard-main-pr workflow for business-canon`,
|
|
110
|
+
});
|
|
111
|
+
ctx.logger.info(` Created guard-main-pr.yml for business-canon.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ctx.logger.info('GitHub setup complete.');
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonSyncTriggerTemplate = canonSyncTriggerTemplate;
|
|
4
|
+
/**
|
|
5
|
+
* Generates the canon-sync-trigger workflow template.
|
|
6
|
+
* On merge to main, creates an issue in the business-canon repo.
|
|
7
|
+
* Only generated for governed repos, NOT for the business-canon repo itself.
|
|
8
|
+
*/
|
|
9
|
+
function canonSyncTriggerTemplate(canonOwnerRepo) {
|
|
10
|
+
return `# Generated by collab-cli — triggers canon sync on merge to main.
|
|
11
|
+
name: Canon Sync Trigger
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
trigger-canon-sync:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Create canon sync issue
|
|
22
|
+
uses: actions/github-script@v7
|
|
23
|
+
with:
|
|
24
|
+
github-token: \${{ secrets.CANON_SYNC_PAT }}
|
|
25
|
+
script: |
|
|
26
|
+
const [owner, repo] = '${canonOwnerRepo}'.split('/');
|
|
27
|
+
await github.rest.issues.create({
|
|
28
|
+
owner, repo,
|
|
29
|
+
title: 'Canon sync required — ' + context.repo.repo,
|
|
30
|
+
body: '## Canon Sync Required\\n\\n'
|
|
31
|
+
+ 'Merge to \\\`main\\\` in **' + context.repo.owner + '/' + context.repo.repo + '**.\\n\\n'
|
|
32
|
+
+ '**Commit:** ' + context.sha + '\\n\\n'
|
|
33
|
+
+ '> Created automatically by canon-sync-trigger.',
|
|
34
|
+
labels: ['canon-sync', 'automated'],
|
|
35
|
+
});
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.guardMainPrTemplate = void 0;
|
|
4
|
+
exports.guardMainPrTemplate = `# Generated by collab-cli — blocks PRs to main that don't come from development.
|
|
5
|
+
name: Guard Main PR Source
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
10
|
+
types: [opened, synchronize, reopened]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check-source-branch:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Verify PR comes from development
|
|
17
|
+
if: github.event.pull_request.head.ref != 'development'
|
|
18
|
+
run: |
|
|
19
|
+
echo "::error::PRs to main must come from the development branch."
|
|
20
|
+
exit 1
|
|
21
|
+
- name: PR source is valid
|
|
22
|
+
if: github.event.pull_request.head.ref == 'development'
|
|
23
|
+
run: echo "PR is from development branch — allowed."
|
|
24
|
+
`;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.architectureMergeTemplate = exports.architecturePrTemplate = void 0;
|
|
3
|
+
exports.canonSyncTriggerTemplate = exports.guardMainPrTemplate = exports.architectureMergeTemplate = exports.architecturePrTemplate = void 0;
|
|
4
4
|
var architecture_pr_1 = require("./architecture-pr");
|
|
5
5
|
Object.defineProperty(exports, "architecturePrTemplate", { enumerable: true, get: function () { return architecture_pr_1.architecturePrTemplate; } });
|
|
6
6
|
var architecture_merge_1 = require("./architecture-merge");
|
|
7
7
|
Object.defineProperty(exports, "architectureMergeTemplate", { enumerable: true, get: function () { return architecture_merge_1.architectureMergeTemplate; } });
|
|
8
|
+
var guard_main_pr_1 = require("./guard-main-pr");
|
|
9
|
+
Object.defineProperty(exports, "guardMainPrTemplate", { enumerable: true, get: function () { return guard_main_pr_1.guardMainPrTemplate; } });
|
|
10
|
+
var canon_sync_trigger_1 = require("./canon-sync-trigger");
|
|
11
|
+
Object.defineProperty(exports, "canonSyncTriggerTemplate", { enumerable: true, get: function () { return canon_sync_trigger_1.canonSyncTriggerTemplate; } });
|