specweave 1.0.235 → 1.0.239
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 +89 -193
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts +37 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +176 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts +36 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.js +115 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts +37 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js +56 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts +68 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.js +102 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts +64 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +162 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.d.ts +50 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.js +107 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +53 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +138 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts +40 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.js +50 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts +30 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.js +75 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts +94 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.js +232 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.d.ts +50 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js +114 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts +53 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.js +109 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts +21 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +161 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +46 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +99 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +153 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -0
- package/dist/plugins/specweave-github/lib/index.d.ts +1 -4
- package/dist/plugins/specweave-github/lib/index.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/index.js +1 -4
- package/dist/plugins/specweave-github/lib/index.js.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +7 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js +15 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +10 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +36 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts +25 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +57 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +7 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.js +17 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -0
- package/dist/src/cli/commands/auto.d.ts.map +1 -1
- package/dist/src/cli/commands/auto.js +1 -2
- package/dist/src/cli/commands/auto.js.map +1 -1
- package/dist/src/cli/commands/cancel-auto.js +1 -2
- package/dist/src/cli/commands/cancel-auto.js.map +1 -1
- package/dist/src/cli/commands/living-docs.js +2 -2
- package/dist/src/cli/commands/living-docs.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +1 -2
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/core/config/types.d.ts +8 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js +3 -0
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/types/sync-profile.d.ts +72 -0
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js +6 -0
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave/hooks/hooks.json +2 -2
- package/plugins/specweave/hooks/startup-health-check.sh +1 -1
- package/plugins/specweave/hooks/stop-auto-v5.sh +166 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +10 -0
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +21 -1
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -1
- package/plugins/specweave/skills/auto/SKILL.md +71 -251
- package/plugins/specweave/skills/team-build/SKILL.md +370 -0
- package/plugins/specweave/skills/team-merge/SKILL.md +123 -0
- package/plugins/specweave/skills/team-orchestrate/SKILL.md +800 -0
- package/plugins/specweave/skills/team-status/SKILL.md +89 -0
- package/plugins/specweave-github/MULTI-PROJECT-SYNC-ARCHITECTURE.md +94 -8
- package/plugins/specweave-github/commands/sync.md +17 -3
- package/plugins/specweave-github/hooks/github-ac-sync-handler.sh +255 -0
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +455 -0
- package/plugins/specweave-github/lib/github-ac-comment-poster.js +150 -0
- package/plugins/specweave-github/lib/github-ac-comment-poster.ts +245 -0
- package/plugins/specweave-github/lib/github-batch-sync.js +93 -0
- package/plugins/specweave-github/lib/github-batch-sync.ts +152 -0
- package/plugins/specweave-github/lib/github-board-resolver-v2.js +47 -0
- package/plugins/specweave-github/lib/github-board-resolver-v2.ts +73 -0
- package/plugins/specweave-github/lib/github-conflict-resolver.js +90 -0
- package/plugins/specweave-github/lib/github-conflict-resolver.ts +154 -0
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +168 -0
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +252 -0
- package/plugins/specweave-github/lib/github-field-sync.js +116 -0
- package/plugins/specweave-github/lib/github-field-sync.ts +165 -0
- package/plugins/specweave-github/lib/github-graphql-client.js +129 -0
- package/plugins/specweave-github/lib/github-graphql-client.ts +181 -0
- package/plugins/specweave-github/lib/github-issue-body-generator.js +30 -0
- package/plugins/specweave-github/lib/github-issue-body-generator.ts +76 -0
- package/plugins/specweave-github/lib/github-issue-body-parser.js +55 -0
- package/plugins/specweave-github/lib/github-issue-body-parser.ts +92 -0
- package/plugins/specweave-github/lib/github-pull-sync.js +185 -0
- package/plugins/specweave-github/lib/github-pull-sync.ts +343 -0
- package/plugins/specweave-github/lib/github-push-sync.js +119 -0
- package/plugins/specweave-github/lib/github-push-sync.ts +174 -0
- package/plugins/specweave-github/lib/github-rate-limiter.js +96 -0
- package/plugins/specweave-github/lib/github-rate-limiter.ts +143 -0
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +117 -0
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +180 -0
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +84 -0
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +156 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.js +134 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +226 -0
- package/plugins/specweave-github/lib/index.js +1 -7
- package/plugins/specweave-github/lib/index.ts +1 -4
- package/plugins/specweave-github/skills/github-sync/SKILL.md +76 -4
- package/plugins/specweave-testing/commands/e2e-setup.md +18 -0
- package/plugins/specweave-testing/commands/ui-automate.md +2 -0
- package/plugins/specweave-testing/commands/ui-inspect.md +8 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +6 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.js +14 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.ts +24 -0
- package/plugins/specweave-testing/lib/playwright-cli-detector.js +33 -0
- package/plugins/specweave-testing/lib/playwright-cli-detector.ts +48 -0
- package/plugins/specweave-testing/lib/playwright-cli-runner.js +58 -0
- package/plugins/specweave-testing/lib/playwright-cli-runner.ts +80 -0
- package/plugins/specweave-testing/lib/playwright-routing.js +16 -0
- package/plugins/specweave-testing/lib/playwright-routing.ts +38 -0
- package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +38 -0
- package/src/templates/CLAUDE.md.template +7 -0
- package/src/templates/config.json.template +9 -1
- package/dist/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
- package/dist/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/subtask-sync.js +0 -147
- package/dist/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
- package/dist/plugins/specweave-github/lib/task-parser.d.ts +0 -37
- package/dist/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/task-parser.js +0 -211
- package/dist/plugins/specweave-github/lib/task-parser.js.map +0 -1
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +0 -56
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +0 -375
- package/dist/plugins/specweave-github/lib/task-sync.js.map +0 -1
- package/plugins/specweave/hooks/validate-completion-conditions.sh +0 -474
- package/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
- package/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
- package/plugins/specweave-github/lib/subtask-sync.js +0 -154
- package/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
- package/plugins/specweave-github/lib/subtask-sync.ts +0 -225
- package/plugins/specweave-github/lib/task-parser.d.js +0 -0
- package/plugins/specweave-github/lib/task-parser.d.ts +0 -37
- package/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
- package/plugins/specweave-github/lib/task-parser.js +0 -195
- package/plugins/specweave-github/lib/task-parser.js.map +0 -1
- package/plugins/specweave-github/lib/task-parser.ts +0 -246
- package/plugins/specweave-github/lib/task-sync.d.js +0 -0
- package/plugins/specweave-github/lib/task-sync.d.ts +0 -51
- package/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
- package/plugins/specweave-github/lib/task-sync.js +0 -415
- package/plugins/specweave-github/lib/task-sync.js.map +0 -1
- package/plugins/specweave-github/lib/task-sync.ts +0 -451
- package/plugins/specweave-github/skills/github-issue-tracker/SKILL.md +0 -496
- /package/plugins/specweave/hooks/{stop-auto.sh → _archive/stop-auto-v4-legacy.sh} +0 -0
- /package/plugins/{specweave-github/lib/subtask-sync.d.js → specweave-testing/lib/playwright-ci-defaults.d.js} +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Conflict Resolver
|
|
3
|
+
*
|
|
4
|
+
* Field-level conflict detection and resolution between spec state
|
|
5
|
+
* and GitHub issue state. Supports title, status, and AC fields.
|
|
6
|
+
*
|
|
7
|
+
* @module github-conflict-resolver
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ConflictResolution = 'github-wins' | 'spec-wins' | 'prompt';
|
|
11
|
+
|
|
12
|
+
export interface ConflictField {
|
|
13
|
+
field: string;
|
|
14
|
+
specValue: string;
|
|
15
|
+
githubValue: string;
|
|
16
|
+
defaultResolution: ConflictResolution;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResolvedConflict {
|
|
20
|
+
field: string;
|
|
21
|
+
specValue: string;
|
|
22
|
+
githubValue: string;
|
|
23
|
+
resolution: ConflictResolution;
|
|
24
|
+
resolvedValue: string;
|
|
25
|
+
resolvedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConflictResolverConfig {
|
|
29
|
+
defaultStatusResolution?: ConflictResolution;
|
|
30
|
+
defaultContentResolution?: ConflictResolution;
|
|
31
|
+
defaultACResolution?: ConflictResolution;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SpecState {
|
|
35
|
+
title: string;
|
|
36
|
+
status: string;
|
|
37
|
+
acceptanceCriteria: Array<{ id: string; completed: boolean }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface GitHubState {
|
|
41
|
+
title: string;
|
|
42
|
+
state: 'open' | 'closed';
|
|
43
|
+
acceptanceCriteria: Array<{ id: string; completed: boolean }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class GitHubConflictResolver {
|
|
47
|
+
private statusResolution: ConflictResolution;
|
|
48
|
+
private contentResolution: ConflictResolution;
|
|
49
|
+
private acResolution: ConflictResolution;
|
|
50
|
+
|
|
51
|
+
constructor(config?: ConflictResolverConfig) {
|
|
52
|
+
this.statusResolution = config?.defaultStatusResolution ?? 'github-wins';
|
|
53
|
+
this.contentResolution = config?.defaultContentResolution ?? 'prompt';
|
|
54
|
+
this.acResolution = config?.defaultACResolution ?? 'github-wins';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Detect conflicts between spec and GitHub states.
|
|
59
|
+
*/
|
|
60
|
+
detectConflicts(specState: SpecState, githubState: GitHubState): ConflictField[] {
|
|
61
|
+
const conflicts: ConflictField[] = [];
|
|
62
|
+
|
|
63
|
+
// Title conflict
|
|
64
|
+
if (specState.title !== githubState.title) {
|
|
65
|
+
conflicts.push({
|
|
66
|
+
field: 'title',
|
|
67
|
+
specValue: specState.title,
|
|
68
|
+
githubValue: githubState.title,
|
|
69
|
+
defaultResolution: this.contentResolution,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Status conflict: map GitHub state to spec status for comparison
|
|
74
|
+
const githubStatus = githubState.state; // 'open' or 'closed'
|
|
75
|
+
const specStatus = specState.status;
|
|
76
|
+
const statusMismatch = this.isStatusConflict(specStatus, githubStatus);
|
|
77
|
+
|
|
78
|
+
if (statusMismatch) {
|
|
79
|
+
conflicts.push({
|
|
80
|
+
field: 'status',
|
|
81
|
+
specValue: specStatus,
|
|
82
|
+
githubValue: githubStatus,
|
|
83
|
+
defaultResolution: this.statusResolution,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// AC conflicts
|
|
88
|
+
const ghAcMap = new Map(
|
|
89
|
+
githubState.acceptanceCriteria.map(ac => [ac.id, ac.completed]),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
for (const specAc of specState.acceptanceCriteria) {
|
|
93
|
+
const ghCompleted = ghAcMap.get(specAc.id);
|
|
94
|
+
if (ghCompleted !== undefined && specAc.completed !== ghCompleted) {
|
|
95
|
+
conflicts.push({
|
|
96
|
+
field: `ac:${specAc.id}`,
|
|
97
|
+
specValue: String(specAc.completed),
|
|
98
|
+
githubValue: String(ghCompleted),
|
|
99
|
+
defaultResolution: this.acResolution,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return conflicts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve all conflicts using their default resolution strategy.
|
|
109
|
+
*/
|
|
110
|
+
resolveConflicts(conflicts: ConflictField[]): ResolvedConflict[] {
|
|
111
|
+
return conflicts.map(c => this.resolveConflict(c));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a single conflict with optional override.
|
|
116
|
+
*/
|
|
117
|
+
resolveConflict(conflict: ConflictField, resolution?: ConflictResolution): ResolvedConflict {
|
|
118
|
+
const effectiveResolution = resolution ?? conflict.defaultResolution;
|
|
119
|
+
let resolvedValue: string;
|
|
120
|
+
|
|
121
|
+
switch (effectiveResolution) {
|
|
122
|
+
case 'github-wins':
|
|
123
|
+
resolvedValue = conflict.githubValue;
|
|
124
|
+
break;
|
|
125
|
+
case 'spec-wins':
|
|
126
|
+
resolvedValue = conflict.specValue;
|
|
127
|
+
break;
|
|
128
|
+
case 'prompt':
|
|
129
|
+
// Prompt keeps spec value pending user decision
|
|
130
|
+
resolvedValue = conflict.specValue;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
field: conflict.field,
|
|
136
|
+
specValue: conflict.specValue,
|
|
137
|
+
githubValue: conflict.githubValue,
|
|
138
|
+
resolution: effectiveResolution,
|
|
139
|
+
resolvedValue,
|
|
140
|
+
resolvedAt: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if spec status and GitHub state represent a conflict.
|
|
146
|
+
*/
|
|
147
|
+
private isStatusConflict(specStatus: string, githubState: string): boolean {
|
|
148
|
+
// "completed" spec + "open" GitHub = conflict
|
|
149
|
+
if (specStatus === 'completed' && githubState === 'open') return true;
|
|
150
|
+
// "planned" or "in-progress" spec + "closed" GitHub = conflict
|
|
151
|
+
if (specStatus !== 'completed' && githubState === 'closed') return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
|
|
2
|
+
import { generateIssueBody } from "./github-issue-body-generator.js";
|
|
3
|
+
async function crossRepoSync(stories, options) {
|
|
4
|
+
const result = {
|
|
5
|
+
created: [],
|
|
6
|
+
updated: [],
|
|
7
|
+
errors: [],
|
|
8
|
+
crossReferences: []
|
|
9
|
+
};
|
|
10
|
+
if (stories.length === 0) return result;
|
|
11
|
+
const env = getEnv(options.token);
|
|
12
|
+
const storyIssueMap = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const story of stories) {
|
|
14
|
+
const repos = story.targetRepos.length > 0 ? story.targetRepos : [`${options.owner}/${options.defaultRepo}`];
|
|
15
|
+
const storyIssues = [];
|
|
16
|
+
for (const repo of repos) {
|
|
17
|
+
try {
|
|
18
|
+
const body = generateIssueBody({
|
|
19
|
+
id: story.id,
|
|
20
|
+
title: story.title,
|
|
21
|
+
description: story.description,
|
|
22
|
+
priority: story.priority,
|
|
23
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
24
|
+
specId: story.specId
|
|
25
|
+
});
|
|
26
|
+
const title = `[${story.id}] ${story.title}`;
|
|
27
|
+
const existing = await searchIssue(story.id, repo, env);
|
|
28
|
+
if (existing) {
|
|
29
|
+
await updateIssue(existing.number, title, body, repo, env);
|
|
30
|
+
result.updated.push({
|
|
31
|
+
userStoryId: story.id,
|
|
32
|
+
repo,
|
|
33
|
+
issueNumber: existing.number,
|
|
34
|
+
issueUrl: `https://github.com/${repo}/issues/${existing.number}`
|
|
35
|
+
});
|
|
36
|
+
storyIssues.push({ repo, issueNumber: existing.number });
|
|
37
|
+
} else {
|
|
38
|
+
const created = await createIssue(title, body, story, repo, env);
|
|
39
|
+
result.created.push({
|
|
40
|
+
userStoryId: story.id,
|
|
41
|
+
repo,
|
|
42
|
+
issueNumber: created.number,
|
|
43
|
+
issueUrl: created.url,
|
|
44
|
+
issueNodeId: created.node_id
|
|
45
|
+
});
|
|
46
|
+
storyIssues.push({ repo, issueNumber: created.number });
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
result.errors.push({
|
|
50
|
+
userStoryId: story.id,
|
|
51
|
+
repo,
|
|
52
|
+
error: err instanceof Error ? err.message : String(err)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (storyIssues.length > 1) {
|
|
57
|
+
storyIssueMap.set(story.id, storyIssues);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const [, issues] of storyIssueMap) {
|
|
61
|
+
for (const issue of issues) {
|
|
62
|
+
const linkedTo = issues.filter((i) => i.repo !== issue.repo);
|
|
63
|
+
result.crossReferences.push({
|
|
64
|
+
repo: issue.repo,
|
|
65
|
+
issueNumber: issue.issueNumber,
|
|
66
|
+
linkedTo
|
|
67
|
+
});
|
|
68
|
+
const crossRefSection = buildCrossRefSection(linkedTo);
|
|
69
|
+
try {
|
|
70
|
+
await appendToIssueBody(issue.issueNumber, issue.repo, crossRefSection, env);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
async function searchIssue(usId, repo, env) {
|
|
78
|
+
const res = await execFileNoThrow("gh", [
|
|
79
|
+
"issue",
|
|
80
|
+
"list",
|
|
81
|
+
"--repo",
|
|
82
|
+
repo,
|
|
83
|
+
"--search",
|
|
84
|
+
`[${usId}] in:title`,
|
|
85
|
+
"--json",
|
|
86
|
+
"number,title,node_id",
|
|
87
|
+
"--limit",
|
|
88
|
+
"1"
|
|
89
|
+
], { env });
|
|
90
|
+
if (!res.success) {
|
|
91
|
+
throw new Error(`Search failed for ${repo}: ${res.stderr}`);
|
|
92
|
+
}
|
|
93
|
+
const issues = JSON.parse(res.stdout);
|
|
94
|
+
return issues.length > 0 ? issues[0] : null;
|
|
95
|
+
}
|
|
96
|
+
async function createIssue(title, body, story, repo, env) {
|
|
97
|
+
const res = await execFileNoThrow("gh", [
|
|
98
|
+
"issue",
|
|
99
|
+
"create",
|
|
100
|
+
"--repo",
|
|
101
|
+
repo,
|
|
102
|
+
"--title",
|
|
103
|
+
title,
|
|
104
|
+
"--body",
|
|
105
|
+
body,
|
|
106
|
+
"--label",
|
|
107
|
+
"user-story",
|
|
108
|
+
"--label",
|
|
109
|
+
`priority:${story.priority}`,
|
|
110
|
+
"--json",
|
|
111
|
+
"number,url,node_id"
|
|
112
|
+
], { env });
|
|
113
|
+
if (!res.success) {
|
|
114
|
+
throw new Error(`Create failed for ${repo}: ${res.stderr}`);
|
|
115
|
+
}
|
|
116
|
+
return JSON.parse(res.stdout);
|
|
117
|
+
}
|
|
118
|
+
async function updateIssue(issueNumber, title, body, repo, env) {
|
|
119
|
+
const res = await execFileNoThrow("gh", [
|
|
120
|
+
"issue",
|
|
121
|
+
"edit",
|
|
122
|
+
String(issueNumber),
|
|
123
|
+
"--repo",
|
|
124
|
+
repo,
|
|
125
|
+
"--title",
|
|
126
|
+
title,
|
|
127
|
+
"--body",
|
|
128
|
+
body,
|
|
129
|
+
"--json",
|
|
130
|
+
"number,url"
|
|
131
|
+
], { env });
|
|
132
|
+
if (!res.success) {
|
|
133
|
+
throw new Error(`Update failed for ${repo}: ${res.stderr}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function appendToIssueBody(issueNumber, repo, section, env) {
|
|
137
|
+
const res = await execFileNoThrow("gh", [
|
|
138
|
+
"issue",
|
|
139
|
+
"edit",
|
|
140
|
+
String(issueNumber),
|
|
141
|
+
"--repo",
|
|
142
|
+
repo,
|
|
143
|
+
"--body",
|
|
144
|
+
section,
|
|
145
|
+
"--json",
|
|
146
|
+
"number,url"
|
|
147
|
+
], { env });
|
|
148
|
+
if (!res.success) {
|
|
149
|
+
throw new Error(`Cross-ref update failed: ${res.stderr}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function buildCrossRefSection(linkedTo) {
|
|
153
|
+
const refs = linkedTo.map((l) => `- ${l.repo}#${l.issueNumber}`).join("\n");
|
|
154
|
+
return `
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
**Also tracked in:**
|
|
158
|
+
${refs}`;
|
|
159
|
+
}
|
|
160
|
+
function getEnv(token) {
|
|
161
|
+
if (token) {
|
|
162
|
+
return { ...process.env, GH_TOKEN: token };
|
|
163
|
+
}
|
|
164
|
+
return process.env;
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
crossRepoSync
|
|
168
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Cross-Repo Sync — Distributed multi-repo issue creation and linking
|
|
3
|
+
*
|
|
4
|
+
* Creates issues in different repos based on user story target repo tags,
|
|
5
|
+
* then adds cross-reference sections for stories spanning multiple repos.
|
|
6
|
+
*
|
|
7
|
+
* @module github-cross-repo-sync
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
|
|
11
|
+
import { generateIssueBody } from './github-issue-body-generator.js';
|
|
12
|
+
|
|
13
|
+
export interface CrossRepoUserStory {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
priority: string;
|
|
18
|
+
status: string;
|
|
19
|
+
acceptanceCriteria: Array<{ id: string; description: string; completed: boolean }>;
|
|
20
|
+
targetRepos: string[];
|
|
21
|
+
specId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CrossRepoSyncOptions {
|
|
25
|
+
owner: string;
|
|
26
|
+
defaultRepo: string;
|
|
27
|
+
token?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CreatedIssue {
|
|
31
|
+
userStoryId: string;
|
|
32
|
+
repo: string;
|
|
33
|
+
issueNumber: number;
|
|
34
|
+
issueUrl: string;
|
|
35
|
+
issueNodeId: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface UpdatedIssue {
|
|
39
|
+
userStoryId: string;
|
|
40
|
+
repo: string;
|
|
41
|
+
issueNumber: number;
|
|
42
|
+
issueUrl: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CrossReference {
|
|
46
|
+
repo: string;
|
|
47
|
+
issueNumber: number;
|
|
48
|
+
linkedTo: Array<{ repo: string; issueNumber: number }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CrossRepoSyncResult {
|
|
52
|
+
created: CreatedIssue[];
|
|
53
|
+
updated: UpdatedIssue[];
|
|
54
|
+
errors: Array<{ userStoryId: string; repo: string; error: string }>;
|
|
55
|
+
crossReferences: CrossReference[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create/update issues across multiple repos and add cross-references.
|
|
60
|
+
*/
|
|
61
|
+
export async function crossRepoSync(
|
|
62
|
+
stories: CrossRepoUserStory[],
|
|
63
|
+
options: CrossRepoSyncOptions,
|
|
64
|
+
): Promise<CrossRepoSyncResult> {
|
|
65
|
+
const result: CrossRepoSyncResult = {
|
|
66
|
+
created: [],
|
|
67
|
+
updated: [],
|
|
68
|
+
errors: [],
|
|
69
|
+
crossReferences: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (stories.length === 0) return result;
|
|
73
|
+
|
|
74
|
+
const env = getEnv(options.token);
|
|
75
|
+
|
|
76
|
+
// Track per-story issues for cross-referencing
|
|
77
|
+
const storyIssueMap = new Map<string, Array<{ repo: string; issueNumber: number }>>();
|
|
78
|
+
|
|
79
|
+
// Step 1: Create/update issues in target repos
|
|
80
|
+
for (const story of stories) {
|
|
81
|
+
const repos = story.targetRepos.length > 0
|
|
82
|
+
? story.targetRepos
|
|
83
|
+
: [`${options.owner}/${options.defaultRepo}`];
|
|
84
|
+
|
|
85
|
+
const storyIssues: Array<{ repo: string; issueNumber: number }> = [];
|
|
86
|
+
|
|
87
|
+
for (const repo of repos) {
|
|
88
|
+
try {
|
|
89
|
+
const body = generateIssueBody({
|
|
90
|
+
id: story.id,
|
|
91
|
+
title: story.title,
|
|
92
|
+
description: story.description,
|
|
93
|
+
priority: story.priority,
|
|
94
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
95
|
+
specId: story.specId,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const title = `[${story.id}] ${story.title}`;
|
|
99
|
+
const existing = await searchIssue(story.id, repo, env);
|
|
100
|
+
|
|
101
|
+
if (existing) {
|
|
102
|
+
await updateIssue(existing.number, title, body, repo, env);
|
|
103
|
+
result.updated.push({
|
|
104
|
+
userStoryId: story.id,
|
|
105
|
+
repo,
|
|
106
|
+
issueNumber: existing.number,
|
|
107
|
+
issueUrl: `https://github.com/${repo}/issues/${existing.number}`,
|
|
108
|
+
});
|
|
109
|
+
storyIssues.push({ repo, issueNumber: existing.number });
|
|
110
|
+
} else {
|
|
111
|
+
const created = await createIssue(title, body, story, repo, env);
|
|
112
|
+
result.created.push({
|
|
113
|
+
userStoryId: story.id,
|
|
114
|
+
repo,
|
|
115
|
+
issueNumber: created.number,
|
|
116
|
+
issueUrl: created.url,
|
|
117
|
+
issueNodeId: created.node_id,
|
|
118
|
+
});
|
|
119
|
+
storyIssues.push({ repo, issueNumber: created.number });
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
result.errors.push({
|
|
123
|
+
userStoryId: story.id,
|
|
124
|
+
repo,
|
|
125
|
+
error: err instanceof Error ? err.message : String(err),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (storyIssues.length > 1) {
|
|
131
|
+
storyIssueMap.set(story.id, storyIssues);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Step 2: Add cross-references for multi-repo stories
|
|
136
|
+
for (const [, issues] of storyIssueMap) {
|
|
137
|
+
for (const issue of issues) {
|
|
138
|
+
const linkedTo = issues.filter(i => i.repo !== issue.repo);
|
|
139
|
+
result.crossReferences.push({
|
|
140
|
+
repo: issue.repo,
|
|
141
|
+
issueNumber: issue.issueNumber,
|
|
142
|
+
linkedTo,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Add cross-reference comment via issue edit (append to body)
|
|
146
|
+
const crossRefSection = buildCrossRefSection(linkedTo);
|
|
147
|
+
try {
|
|
148
|
+
await appendToIssueBody(issue.issueNumber, issue.repo, crossRefSection, env);
|
|
149
|
+
} catch {
|
|
150
|
+
// Non-fatal: cross-ref is nice-to-have
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function searchIssue(
|
|
159
|
+
usId: string,
|
|
160
|
+
repo: string,
|
|
161
|
+
env: NodeJS.ProcessEnv,
|
|
162
|
+
): Promise<{ number: number; title: string; node_id: string } | null> {
|
|
163
|
+
const res = await execFileNoThrow('gh', [
|
|
164
|
+
'issue', 'list',
|
|
165
|
+
'--repo', repo,
|
|
166
|
+
'--search', `[${usId}] in:title`,
|
|
167
|
+
'--json', 'number,title,node_id',
|
|
168
|
+
'--limit', '1',
|
|
169
|
+
], { env });
|
|
170
|
+
|
|
171
|
+
if (!res.success) {
|
|
172
|
+
throw new Error(`Search failed for ${repo}: ${res.stderr}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const issues = JSON.parse(res.stdout);
|
|
176
|
+
return issues.length > 0 ? issues[0] : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function createIssue(
|
|
180
|
+
title: string,
|
|
181
|
+
body: string,
|
|
182
|
+
story: CrossRepoUserStory,
|
|
183
|
+
repo: string,
|
|
184
|
+
env: NodeJS.ProcessEnv,
|
|
185
|
+
): Promise<{ number: number; url: string; node_id: string }> {
|
|
186
|
+
const res = await execFileNoThrow('gh', [
|
|
187
|
+
'issue', 'create',
|
|
188
|
+
'--repo', repo,
|
|
189
|
+
'--title', title,
|
|
190
|
+
'--body', body,
|
|
191
|
+
'--label', 'user-story',
|
|
192
|
+
'--label', `priority:${story.priority}`,
|
|
193
|
+
'--json', 'number,url,node_id',
|
|
194
|
+
], { env });
|
|
195
|
+
|
|
196
|
+
if (!res.success) {
|
|
197
|
+
throw new Error(`Create failed for ${repo}: ${res.stderr}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return JSON.parse(res.stdout);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function updateIssue(
|
|
204
|
+
issueNumber: number,
|
|
205
|
+
title: string,
|
|
206
|
+
body: string,
|
|
207
|
+
repo: string,
|
|
208
|
+
env: NodeJS.ProcessEnv,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const res = await execFileNoThrow('gh', [
|
|
211
|
+
'issue', 'edit', String(issueNumber),
|
|
212
|
+
'--repo', repo,
|
|
213
|
+
'--title', title,
|
|
214
|
+
'--body', body,
|
|
215
|
+
'--json', 'number,url',
|
|
216
|
+
], { env });
|
|
217
|
+
|
|
218
|
+
if (!res.success) {
|
|
219
|
+
throw new Error(`Update failed for ${repo}: ${res.stderr}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function appendToIssueBody(
|
|
224
|
+
issueNumber: number,
|
|
225
|
+
repo: string,
|
|
226
|
+
section: string,
|
|
227
|
+
env: NodeJS.ProcessEnv,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
// Use gh issue edit with --add-body
|
|
230
|
+
const res = await execFileNoThrow('gh', [
|
|
231
|
+
'issue', 'edit', String(issueNumber),
|
|
232
|
+
'--repo', repo,
|
|
233
|
+
'--body', section,
|
|
234
|
+
'--json', 'number,url',
|
|
235
|
+
], { env });
|
|
236
|
+
|
|
237
|
+
if (!res.success) {
|
|
238
|
+
throw new Error(`Cross-ref update failed: ${res.stderr}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildCrossRefSection(linkedTo: Array<{ repo: string; issueNumber: number }>): string {
|
|
243
|
+
const refs = linkedTo.map(l => `- ${l.repo}#${l.issueNumber}`).join('\n');
|
|
244
|
+
return `\n\n---\n**Also tracked in:**\n${refs}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getEnv(token?: string): NodeJS.ProcessEnv {
|
|
248
|
+
if (token) {
|
|
249
|
+
return { ...process.env, GH_TOKEN: token };
|
|
250
|
+
}
|
|
251
|
+
return process.env;
|
|
252
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const DEFAULT_STATUS_MAPPING = {
|
|
2
|
+
"planned": "Todo",
|
|
3
|
+
"in-progress": "In Progress",
|
|
4
|
+
"completed": "Done"
|
|
5
|
+
};
|
|
6
|
+
const DEFAULT_PRIORITY_MAPPING = {
|
|
7
|
+
"P1": "Urgent",
|
|
8
|
+
"P2": "High",
|
|
9
|
+
"P3": "Medium",
|
|
10
|
+
"P4": "Low"
|
|
11
|
+
};
|
|
12
|
+
class GitHubFieldSync {
|
|
13
|
+
constructor(client, config) {
|
|
14
|
+
this.fieldsLoaded = false;
|
|
15
|
+
this.client = client;
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load project fields and cache field IDs and option IDs.
|
|
20
|
+
*/
|
|
21
|
+
async loadFields() {
|
|
22
|
+
if (this.fieldsLoaded) return;
|
|
23
|
+
const fields = await this.client.getProjectFields(this.config.projectId);
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
if (field.name === "Status" && field.options) {
|
|
26
|
+
this.statusField = {
|
|
27
|
+
id: field.id,
|
|
28
|
+
name: field.name,
|
|
29
|
+
options: new Map(field.options.map((o) => [o.name, o.id]))
|
|
30
|
+
};
|
|
31
|
+
} else if (field.name === "Priority" && field.options) {
|
|
32
|
+
this.priorityField = {
|
|
33
|
+
id: field.id,
|
|
34
|
+
name: field.name,
|
|
35
|
+
options: new Map(field.options.map((o) => [o.name, o.id]))
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.fieldsLoaded = true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sync fields for one or more items.
|
|
43
|
+
* Auto-calls loadFields if not called yet.
|
|
44
|
+
*/
|
|
45
|
+
async syncItemFields(items) {
|
|
46
|
+
if (!this.fieldsLoaded) {
|
|
47
|
+
await this.loadFields();
|
|
48
|
+
}
|
|
49
|
+
const result = { updated: [], warnings: [] };
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
if (item.status !== void 0) {
|
|
52
|
+
await this.syncField(
|
|
53
|
+
item,
|
|
54
|
+
"Status",
|
|
55
|
+
item.status,
|
|
56
|
+
this.statusField,
|
|
57
|
+
this.config.statusFieldMapping ?? DEFAULT_STATUS_MAPPING,
|
|
58
|
+
result
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (item.priority !== void 0) {
|
|
62
|
+
await this.syncField(
|
|
63
|
+
item,
|
|
64
|
+
"Priority",
|
|
65
|
+
item.priority,
|
|
66
|
+
this.priorityField,
|
|
67
|
+
this.config.priorityFieldMapping ?? DEFAULT_PRIORITY_MAPPING,
|
|
68
|
+
result
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async syncField(item, fieldName, value, cachedField, mapping, result) {
|
|
75
|
+
if (!cachedField) {
|
|
76
|
+
result.warnings.push({
|
|
77
|
+
itemId: item.itemId,
|
|
78
|
+
field: fieldName,
|
|
79
|
+
message: `${fieldName} field not found on project`
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const mappedValue = mapping[value];
|
|
84
|
+
if (!mappedValue) {
|
|
85
|
+
result.warnings.push({
|
|
86
|
+
itemId: item.itemId,
|
|
87
|
+
field: fieldName,
|
|
88
|
+
message: `No mapping found for ${fieldName} value "${value}"`
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const optionId = cachedField.options.get(mappedValue);
|
|
93
|
+
if (!optionId) {
|
|
94
|
+
result.warnings.push({
|
|
95
|
+
itemId: item.itemId,
|
|
96
|
+
field: fieldName,
|
|
97
|
+
message: `Option "${mappedValue}" not found on ${fieldName} field`
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await this.client.updateItemFieldValue(
|
|
102
|
+
this.config.projectId,
|
|
103
|
+
item.itemId,
|
|
104
|
+
cachedField.id,
|
|
105
|
+
{ singleSelectOptionId: optionId }
|
|
106
|
+
);
|
|
107
|
+
result.updated.push({
|
|
108
|
+
itemId: item.itemId,
|
|
109
|
+
field: fieldName,
|
|
110
|
+
value: mappedValue
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
GitHubFieldSync
|
|
116
|
+
};
|