specweave 1.0.350 → 1.0.352
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/bin/specweave.js +9 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts +5 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js +61 -23
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +3 -2
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +2 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +25 -9
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +14 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +10 -7
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +7 -5
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +13 -3
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts +24 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +36 -20
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +4 -2
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +38 -9
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +32 -22
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +144 -8
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts +8 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.js +94 -24
- package/dist/plugins/specweave-github/lib/github-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.js +29 -9
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts +59 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js +159 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts +45 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js +92 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +13 -28
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +2 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +19 -7
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts +47 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js +110 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts +26 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js +77 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +17 -2
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +103 -33
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +4 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +19 -6
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts +29 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js +73 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts +15 -2
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js +121 -33
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +23 -18
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/sync-progress.d.ts +6 -0
- package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-progress.js +37 -0
- package/dist/src/cli/commands/sync-progress.js.map +1 -1
- package/dist/src/cli/commands/sync-task.d.ts +16 -0
- package/dist/src/cli/commands/sync-task.d.ts.map +1 -0
- package/dist/src/cli/commands/sync-task.js +42 -0
- package/dist/src/cli/commands/sync-task.js.map +1 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js +3 -3
- package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts +9 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.js +26 -8
- package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts +13 -0
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +144 -17
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.js +2 -1
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
- package/dist/src/core/increment/status-commands.d.ts.map +1 -1
- package/dist/src/core/increment/status-commands.js +33 -11
- package/dist/src/core/increment/status-commands.js.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +2 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/locales/de/cli.json +252 -77
- package/dist/src/locales/en/cli.json +7 -0
- package/dist/src/locales/es/cli.json +245 -3
- package/dist/src/locales/fr/cli.json +259 -84
- package/dist/src/locales/ja/cli.json +253 -78
- package/dist/src/locales/ko/cli.json +253 -78
- package/dist/src/locales/pt/cli.json +252 -77
- package/dist/src/locales/ru/cli.json +17 -3
- package/dist/src/locales/zh/cli.json +258 -83
- package/dist/src/sync/ado-reconciler.d.ts.map +1 -1
- package/dist/src/sync/ado-reconciler.js +5 -1
- package/dist/src/sync/ado-reconciler.js.map +1 -1
- package/dist/src/sync/base-reconciler.d.ts.map +1 -1
- package/dist/src/sync/base-reconciler.js +6 -1
- package/dist/src/sync/base-reconciler.js.map +1 -1
- package/dist/src/sync/config.d.ts +4 -0
- package/dist/src/sync/config.d.ts.map +1 -1
- package/dist/src/sync/config.js +6 -4
- package/dist/src/sync/config.js.map +1 -1
- package/dist/src/sync/external-issue-auto-creator.d.ts +3 -0
- package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
- package/dist/src/sync/external-issue-auto-creator.js +53 -17
- package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
- package/dist/src/sync/external-item-sync-service.d.ts +9 -0
- package/dist/src/sync/external-item-sync-service.d.ts.map +1 -1
- package/dist/src/sync/external-item-sync-service.js +210 -9
- package/dist/src/sync/external-item-sync-service.js.map +1 -1
- package/dist/src/sync/github-reconciler.d.ts +30 -0
- package/dist/src/sync/github-reconciler.d.ts.map +1 -1
- package/dist/src/sync/github-reconciler.js +242 -3
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/dist/src/sync/jira-reconciler.d.ts.map +1 -1
- package/dist/src/sync/jira-reconciler.js +5 -1
- package/dist/src/sync/jira-reconciler.js.map +1 -1
- package/dist/src/sync/provider-router.d.ts.map +1 -1
- package/dist/src/sync/provider-router.js +2 -1
- package/dist/src/sync/provider-router.js.map +1 -1
- package/dist/src/sync/providers/ado.d.ts +4 -0
- package/dist/src/sync/providers/ado.d.ts.map +1 -1
- package/dist/src/sync/providers/ado.js +36 -11
- package/dist/src/sync/providers/ado.js.map +1 -1
- package/dist/src/sync/providers/github.d.ts.map +1 -1
- package/dist/src/sync/providers/github.js +48 -35
- package/dist/src/sync/providers/github.js.map +1 -1
- package/dist/src/sync/providers/jira.d.ts.map +1 -1
- package/dist/src/sync/providers/jira.js +42 -26
- package/dist/src/sync/providers/jira.js.map +1 -1
- package/dist/src/sync/status-mapper.d.ts +3 -1
- package/dist/src/sync/status-mapper.d.ts.map +1 -1
- package/dist/src/sync/status-mapper.js +10 -2
- package/dist/src/sync/status-mapper.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +29 -19
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +31 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +13 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +144 -17
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.d.ts +30 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +242 -3
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/skills/architect/SKILL.md +2 -0
- package/plugins/specweave/skills/grill/SKILL.md +2 -0
- package/plugins/specweave/skills/team-lead/SKILL.md +43 -320
- package/plugins/specweave/skills/team-lead/agents/backend.md +60 -0
- package/plugins/specweave/skills/team-lead/agents/database.md +51 -0
- package/plugins/specweave/skills/team-lead/agents/frontend.md +61 -0
- package/plugins/specweave/skills/team-lead/agents/security.md +52 -0
- package/plugins/specweave/skills/team-lead/agents/testing.md +57 -0
- package/plugins/specweave/skills/test-aware-planner/SKILL.md +2 -0
- package/plugins/specweave-ado/hooks/post-task-completion.sh +2 -2
- package/plugins/specweave-ado/lib/ado-client-v2.js +51 -21
- package/plugins/specweave-ado/lib/ado-client-v2.ts +62 -23
- package/plugins/specweave-ado/lib/ado-duplicate-detector.js +4 -4
- package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +4 -3
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.js +54 -12
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +88 -18
- package/plugins/specweave-ado/lib/ado-profile-resolver.js +1 -1
- package/plugins/specweave-ado/lib/ado-profile-resolver.ts +3 -1
- package/plugins/specweave-ado/lib/ado-spec-sync.js +22 -9
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +27 -9
- package/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/plugins/specweave-ado/lib/conflict-resolver.ts +17 -1
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +11 -1
- package/plugins/specweave-ado/lib/per-us-sync.js +8 -1
- package/plugins/specweave-ado/lib/per-us-sync.ts +17 -2
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +28 -2
- package/plugins/specweave-github/hooks/post-task-completion.sh +6 -3
- package/plugins/specweave-github/lib/enhanced-github-sync.js +35 -6
- package/plugins/specweave-github/lib/github-board-resolver.js +4 -4
- package/plugins/specweave-github/lib/github-board-resolver.ts +4 -4
- package/plugins/specweave-github/lib/github-client-v2.js +6 -6
- package/plugins/specweave-github/lib/github-client-v2.ts +11 -7
- package/plugins/specweave-github/lib/github-client.js +5 -4
- package/plugins/specweave-github/lib/github-client.ts +7 -5
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +17 -3
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +16 -3
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +20 -11
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +42 -20
- package/plugins/specweave-github/lib/github-feature-sync.js +32 -8
- package/plugins/specweave-github/lib/github-feature-sync.ts +41 -9
- package/plugins/specweave-github/lib/github-graphql-client.js +29 -20
- package/plugins/specweave-github/lib/github-graphql-client.ts +34 -22
- package/plugins/specweave-github/lib/github-hierarchical-sync.js +2 -2
- package/plugins/specweave-github/lib/github-hierarchical-sync.ts +2 -2
- package/plugins/specweave-github/lib/github-multi-project-sync.js +23 -7
- package/plugins/specweave-github/lib/github-multi-project-sync.ts +26 -8
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +110 -5
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +135 -9
- package/plugins/specweave-github/lib/github-spec-sync.js +85 -24
- package/plugins/specweave-github/lib/github-spec-sync.ts +100 -26
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +3 -1
- package/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +43 -0
- package/plugins/specweave-github/lib/per-us-sync.js +26 -11
- package/plugins/specweave-github/lib/per-us-sync.ts +29 -11
- package/plugins/specweave-jira/hooks/post-task-completion.sh +2 -1
- package/plugins/specweave-jira/lib/content-format-adapter.js +116 -0
- package/plugins/specweave-jira/lib/content-format-adapter.ts +189 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +21 -5
- package/plugins/specweave-jira/lib/jira-deployment-detector.js +63 -0
- package/plugins/specweave-jira/lib/jira-deployment-detector.ts +113 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.js +12 -29
- package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +13 -27
- package/plugins/specweave-jira/lib/jira-epic-sync.js +15 -5
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +22 -7
- package/plugins/specweave-jira/lib/jira-field-discovery.js +76 -0
- package/plugins/specweave-jira/lib/jira-field-discovery.ts +139 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.js +10 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +11 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.js +19 -9
- package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +25 -14
- package/plugins/specweave-jira/lib/jira-paginated-search.js +55 -0
- package/plugins/specweave-jira/lib/jira-paginated-search.ts +108 -0
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.ts +5 -3
- package/plugins/specweave-jira/lib/jira-spec-sync.js +102 -31
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +123 -45
- package/plugins/specweave-jira/lib/jira-status-sync.js +18 -5
- package/plugins/specweave-jira/lib/jira-status-sync.ts +21 -6
- package/plugins/specweave-jira/lib/metadata-paths.js +38 -0
- package/plugins/specweave-jira/lib/metadata-paths.ts +73 -0
- package/plugins/specweave-jira/lib/reorganization-detector.js +101 -23
- package/plugins/specweave-jira/lib/reorganization-detector.ts +125 -35
- package/plugins/specweave-jira/scripts/refresh-cache.js +1 -1
- package/plugins/specweave-jira/scripts/refresh-cache.ts +2 -2
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +3 -5
|
@@ -12,6 +12,7 @@ import * as fs from '../../../src/utils/fs-native.js';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import * as yaml from 'yaml';
|
|
14
14
|
import { JiraClient, JiraIssue, JiraIssueCreate } from '../../../src/integrations/jira/jira-client.js';
|
|
15
|
+
import { toDescription } from './content-format-adapter.js';
|
|
15
16
|
|
|
16
17
|
interface EpicFrontmatter {
|
|
17
18
|
id: string;
|
|
@@ -77,11 +78,13 @@ export class JiraEpicSync {
|
|
|
77
78
|
private client: JiraClient;
|
|
78
79
|
private specsDir: string;
|
|
79
80
|
private projectKey: string;
|
|
81
|
+
private domain: string;
|
|
80
82
|
|
|
81
|
-
constructor(client: JiraClient, specsDir: string, projectKey: string) {
|
|
83
|
+
constructor(client: JiraClient, specsDir: string, projectKey: string, domain?: string) {
|
|
82
84
|
this.client = client;
|
|
83
85
|
this.specsDir = specsDir;
|
|
84
86
|
this.projectKey = projectKey;
|
|
87
|
+
this.domain = domain || (client as any)['credentials']?.domain || '';
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -193,11 +196,21 @@ export class JiraEpicSync {
|
|
|
193
196
|
* Find Epic folder by ID (FS-001 or just 001)
|
|
194
197
|
*/
|
|
195
198
|
private async findEpicFolder(epicId: string): Promise<string | null> {
|
|
196
|
-
const
|
|
199
|
+
const folders = await fs.readdir(this.specsDir);
|
|
200
|
+
|
|
201
|
+
// Try exact match first
|
|
202
|
+
for (const folder of folders) {
|
|
203
|
+
if (folder.startsWith(epicId)) {
|
|
204
|
+
return path.join(this.specsDir, folder);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try with project key prefix (derived from projectKey, not hardcoded FS-)
|
|
209
|
+
const prefix = `${this.projectKey}-`;
|
|
210
|
+
const normalizedId = epicId.startsWith(prefix)
|
|
197
211
|
? epicId
|
|
198
|
-
:
|
|
212
|
+
: `${prefix}${epicId.padStart(3, '0')}`;
|
|
199
213
|
|
|
200
|
-
const folders = await fs.readdir(this.specsDir);
|
|
201
214
|
for (const folder of folders) {
|
|
202
215
|
if (folder.startsWith(normalizedId)) {
|
|
203
216
|
return path.join(this.specsDir, folder);
|
|
@@ -274,7 +287,8 @@ export class JiraEpicSync {
|
|
|
274
287
|
url: string;
|
|
275
288
|
}> {
|
|
276
289
|
const summary = `[${epic.id}] ${epic.title}`;
|
|
277
|
-
const
|
|
290
|
+
const rawDescription = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
|
|
291
|
+
const description = toDescription(rawDescription, this.domain) as any;
|
|
278
292
|
|
|
279
293
|
const issueData: JiraIssueCreate = {
|
|
280
294
|
issueType: 'Epic',
|
|
@@ -288,7 +302,7 @@ export class JiraEpicSync {
|
|
|
288
302
|
|
|
289
303
|
return {
|
|
290
304
|
key: issue.key,
|
|
291
|
-
url: issue.self.replace(
|
|
305
|
+
url: issue.self.replace(/\/rest\/api\/\d+\/issue\//, '/browse/'),
|
|
292
306
|
};
|
|
293
307
|
}
|
|
294
308
|
|
|
@@ -297,7 +311,8 @@ export class JiraEpicSync {
|
|
|
297
311
|
*/
|
|
298
312
|
private async updateEpic(epicKey: string, epic: EpicFrontmatter): Promise<void> {
|
|
299
313
|
const summary = `[${epic.id}] ${epic.title}`;
|
|
300
|
-
const
|
|
314
|
+
const rawDescription = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
|
|
315
|
+
const description = toDescription(rawDescription, this.domain) as any;
|
|
301
316
|
|
|
302
317
|
await this.client.updateIssue({
|
|
303
318
|
key: epicKey,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { getApiBaseUrl } from "./jira-deployment-detector.js";
|
|
3
|
+
const epicLinkFieldCache = /* @__PURE__ */ new Map();
|
|
4
|
+
const projectStyleCache = /* @__PURE__ */ new Map();
|
|
5
|
+
async function discoverEpicLinkField(domain, auth) {
|
|
6
|
+
const cached = epicLinkFieldCache.get(domain);
|
|
7
|
+
if (cached !== void 0) return cached;
|
|
8
|
+
try {
|
|
9
|
+
const baseUrl = getApiBaseUrl(domain);
|
|
10
|
+
const response = await axios.get(`${baseUrl}/field`, {
|
|
11
|
+
auth: {
|
|
12
|
+
username: auth.email,
|
|
13
|
+
password: auth.apiToken
|
|
14
|
+
},
|
|
15
|
+
headers: { Accept: "application/json" },
|
|
16
|
+
timeout: 1e4
|
|
17
|
+
});
|
|
18
|
+
const fields = response.data;
|
|
19
|
+
const epicLinkField = fields.find(
|
|
20
|
+
(f) => f.name === "Epic Link" || f.schema?.custom === "com.pyxis.greenhopper.jira:gh-epic-link"
|
|
21
|
+
);
|
|
22
|
+
const fieldId = epicLinkField?.id || null;
|
|
23
|
+
epicLinkFieldCache.set(domain, fieldId);
|
|
24
|
+
return fieldId;
|
|
25
|
+
} catch {
|
|
26
|
+
epicLinkFieldCache.set(domain, null);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function detectProjectStyle(domain, projectKey, auth) {
|
|
31
|
+
const cacheKey = `${domain}:${projectKey}`;
|
|
32
|
+
const cached = projectStyleCache.get(cacheKey);
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
try {
|
|
35
|
+
const baseUrl = getApiBaseUrl(domain);
|
|
36
|
+
const response = await axios.get(`${baseUrl}/project/${projectKey}`, {
|
|
37
|
+
auth: {
|
|
38
|
+
username: auth.email,
|
|
39
|
+
password: auth.apiToken
|
|
40
|
+
},
|
|
41
|
+
headers: { Accept: "application/json" },
|
|
42
|
+
timeout: 1e4
|
|
43
|
+
});
|
|
44
|
+
const project = response.data;
|
|
45
|
+
const isNextGen = project.style === "next-gen" || project.simplified === true;
|
|
46
|
+
const style = isNextGen ? "next-gen" : "classic";
|
|
47
|
+
projectStyleCache.set(cacheKey, style);
|
|
48
|
+
return style;
|
|
49
|
+
} catch {
|
|
50
|
+
projectStyleCache.set(cacheKey, "classic");
|
|
51
|
+
return "classic";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function getEpicLinkFieldForProject(domain, projectKey, auth) {
|
|
55
|
+
const style = await detectProjectStyle(domain, projectKey, auth);
|
|
56
|
+
if (style === "next-gen") {
|
|
57
|
+
return { field: "parent", style };
|
|
58
|
+
}
|
|
59
|
+
const epicLinkFieldId = await discoverEpicLinkField(domain, auth);
|
|
60
|
+
if (!epicLinkFieldId) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Could not discover Epic Link custom field for ${domain}. Ensure the JIRA instance has the Epic Link field and API credentials have field read access.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return { field: epicLinkFieldId, style };
|
|
66
|
+
}
|
|
67
|
+
function clearFieldDiscoveryCache() {
|
|
68
|
+
epicLinkFieldCache.clear();
|
|
69
|
+
projectStyleCache.clear();
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
clearFieldDiscoveryCache,
|
|
73
|
+
detectProjectStyle,
|
|
74
|
+
discoverEpicLinkField,
|
|
75
|
+
getEpicLinkFieldForProject
|
|
76
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA Field Discovery
|
|
3
|
+
*
|
|
4
|
+
* Dynamically discovers custom field IDs (e.g., Epic Link) and
|
|
5
|
+
* detects project style (Next-gen vs Classic).
|
|
6
|
+
*
|
|
7
|
+
* Results are cached per domain/project to avoid repeated lookups.
|
|
8
|
+
*
|
|
9
|
+
* @module jira-field-discovery
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import axios from 'axios';
|
|
13
|
+
import { getApiBaseUrl } from './jira-deployment-detector.js';
|
|
14
|
+
|
|
15
|
+
/** Cache: domain → epic link field ID */
|
|
16
|
+
const epicLinkFieldCache = new Map<string, string | null>();
|
|
17
|
+
|
|
18
|
+
/** Cache: projectKey → project style */
|
|
19
|
+
const projectStyleCache = new Map<string, 'next-gen' | 'classic'>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Discover the Epic Link custom field ID by querying /rest/api/field.
|
|
23
|
+
* Searches for fields with name "Epic Link" or schema type "com.pyxis.greenhopper.jira:gh-epic-link".
|
|
24
|
+
*
|
|
25
|
+
* @returns The custom field ID (e.g., "customfield_10014") or null if not found
|
|
26
|
+
*/
|
|
27
|
+
export async function discoverEpicLinkField(
|
|
28
|
+
domain: string,
|
|
29
|
+
auth: { email: string; apiToken: string }
|
|
30
|
+
): Promise<string | null> {
|
|
31
|
+
// Check cache
|
|
32
|
+
const cached = epicLinkFieldCache.get(domain);
|
|
33
|
+
if (cached !== undefined) return cached;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const baseUrl = getApiBaseUrl(domain);
|
|
37
|
+
const response = await axios.get(`${baseUrl}/field`, {
|
|
38
|
+
auth: {
|
|
39
|
+
username: auth.email,
|
|
40
|
+
password: auth.apiToken,
|
|
41
|
+
},
|
|
42
|
+
headers: { Accept: 'application/json' },
|
|
43
|
+
timeout: 10000,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const fields: any[] = response.data;
|
|
47
|
+
|
|
48
|
+
// Find Epic Link field by name or schema
|
|
49
|
+
const epicLinkField = fields.find(
|
|
50
|
+
(f: any) =>
|
|
51
|
+
f.name === 'Epic Link' ||
|
|
52
|
+
f.schema?.custom === 'com.pyxis.greenhopper.jira:gh-epic-link'
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const fieldId = epicLinkField?.id || null;
|
|
56
|
+
epicLinkFieldCache.set(domain, fieldId);
|
|
57
|
+
return fieldId;
|
|
58
|
+
} catch {
|
|
59
|
+
epicLinkFieldCache.set(domain, null);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect whether a JIRA project uses Next-gen (Team-managed) or Classic (Company-managed) style.
|
|
66
|
+
*
|
|
67
|
+
* Next-gen projects use `parent` field for epic linking.
|
|
68
|
+
* Classic projects use a custom field (Epic Link).
|
|
69
|
+
*/
|
|
70
|
+
export async function detectProjectStyle(
|
|
71
|
+
domain: string,
|
|
72
|
+
projectKey: string,
|
|
73
|
+
auth: { email: string; apiToken: string }
|
|
74
|
+
): Promise<'next-gen' | 'classic'> {
|
|
75
|
+
const cacheKey = `${domain}:${projectKey}`;
|
|
76
|
+
const cached = projectStyleCache.get(cacheKey);
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const baseUrl = getApiBaseUrl(domain);
|
|
81
|
+
const response = await axios.get(`${baseUrl}/project/${projectKey}`, {
|
|
82
|
+
auth: {
|
|
83
|
+
username: auth.email,
|
|
84
|
+
password: auth.apiToken,
|
|
85
|
+
},
|
|
86
|
+
headers: { Accept: 'application/json' },
|
|
87
|
+
timeout: 10000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const project = response.data;
|
|
91
|
+
|
|
92
|
+
// Next-gen projects have style: "next-gen" or projectTypeKey: "software" with simplified: true
|
|
93
|
+
const isNextGen =
|
|
94
|
+
project.style === 'next-gen' ||
|
|
95
|
+
project.simplified === true;
|
|
96
|
+
|
|
97
|
+
const style = isNextGen ? 'next-gen' : 'classic';
|
|
98
|
+
projectStyleCache.set(cacheKey, style);
|
|
99
|
+
return style;
|
|
100
|
+
} catch {
|
|
101
|
+
// Default to classic if detection fails
|
|
102
|
+
projectStyleCache.set(cacheKey, 'classic');
|
|
103
|
+
return 'classic';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the correct field name/ID for linking an issue to an epic,
|
|
109
|
+
* based on project style.
|
|
110
|
+
*
|
|
111
|
+
* - Next-gen: uses `parent` field
|
|
112
|
+
* - Classic: uses the discovered Epic Link custom field ID
|
|
113
|
+
*/
|
|
114
|
+
export async function getEpicLinkFieldForProject(
|
|
115
|
+
domain: string,
|
|
116
|
+
projectKey: string,
|
|
117
|
+
auth: { email: string; apiToken: string }
|
|
118
|
+
): Promise<{ field: 'parent' | string; style: 'next-gen' | 'classic' }> {
|
|
119
|
+
const style = await detectProjectStyle(domain, projectKey, auth);
|
|
120
|
+
|
|
121
|
+
if (style === 'next-gen') {
|
|
122
|
+
return { field: 'parent', style };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const epicLinkFieldId = await discoverEpicLinkField(domain, auth);
|
|
126
|
+
if (!epicLinkFieldId) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Could not discover Epic Link custom field for ${domain}. ` +
|
|
129
|
+
`Ensure the JIRA instance has the Epic Link field and API credentials have field read access.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return { field: epicLinkFieldId, style };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Clear caches (for testing) */
|
|
136
|
+
export function clearFieldDiscoveryCache(): void {
|
|
137
|
+
epicLinkFieldCache.clear();
|
|
138
|
+
projectStyleCache.clear();
|
|
139
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isCustomStrategy
|
|
5
5
|
} from "../../../src/core/types/sync-profile.js";
|
|
6
6
|
import { getBoardIds } from "./jira-board-resolver.js";
|
|
7
|
+
import { searchAllIssues } from "./jira-paginated-search.js";
|
|
7
8
|
async function buildHierarchicalJQL(client, containers) {
|
|
8
9
|
const clauses = [];
|
|
9
10
|
for (const container of containers) {
|
|
@@ -107,6 +108,9 @@ async function fetchIssuesSimple(client, config, timeRange) {
|
|
|
107
108
|
let jql = `project=${projectKey}`;
|
|
108
109
|
jql = addTimeRangeFilter(jql, timeRange);
|
|
109
110
|
console.log("\u{1F50D} Fetching issues (SIMPLE strategy):", jql);
|
|
111
|
+
if (typeof client.getAxiosClient === "function") {
|
|
112
|
+
return searchAllIssues(client.getAxiosClient(), { jql });
|
|
113
|
+
}
|
|
110
114
|
return client.searchIssues({ jql });
|
|
111
115
|
}
|
|
112
116
|
async function fetchIssuesCustom(client, config, timeRange) {
|
|
@@ -116,6 +120,9 @@ async function fetchIssuesCustom(client, config, timeRange) {
|
|
|
116
120
|
}
|
|
117
121
|
const jql = addTimeRangeFilter(customQuery, timeRange);
|
|
118
122
|
console.log("\u{1F50D} Fetching issues (CUSTOM strategy):", jql);
|
|
123
|
+
if (typeof client.getAxiosClient === "function") {
|
|
124
|
+
return searchAllIssues(client.getAxiosClient(), { jql });
|
|
125
|
+
}
|
|
119
126
|
return client.searchIssues({ jql });
|
|
120
127
|
}
|
|
121
128
|
async function fetchIssuesFiltered(client, config, timeRange) {
|
|
@@ -126,6 +133,9 @@ async function fetchIssuesFiltered(client, config, timeRange) {
|
|
|
126
133
|
const baseJql = await buildHierarchicalJQL(client, containers);
|
|
127
134
|
const jql = addTimeRangeFilter(baseJql, timeRange);
|
|
128
135
|
console.log("\u{1F50D} Fetching issues (FILTERED strategy):", jql);
|
|
136
|
+
if (typeof client.getAxiosClient === "function") {
|
|
137
|
+
return searchAllIssues(client.getAxiosClient(), { jql });
|
|
138
|
+
}
|
|
129
139
|
return client.searchIssues({ jql });
|
|
130
140
|
}
|
|
131
141
|
export {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../../../src/core/types/sync-profile.js';
|
|
18
18
|
import { JiraClient, JiraIssue } from '../../../src/integrations/jira/jira-client.js';
|
|
19
19
|
import { getBoardIds } from './jira-board-resolver.js';
|
|
20
|
+
import { searchAllIssues } from './jira-paginated-search.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Build hierarchical JQL query from containers
|
|
@@ -222,6 +223,10 @@ async function fetchIssuesSimple(
|
|
|
222
223
|
|
|
223
224
|
console.log('🔍 Fetching issues (SIMPLE strategy):', jql);
|
|
224
225
|
|
|
226
|
+
// Use paginated search if client exposes axios instance, otherwise fallback
|
|
227
|
+
if (typeof (client as any).getAxiosClient === 'function') {
|
|
228
|
+
return searchAllIssues((client as any).getAxiosClient(), { jql });
|
|
229
|
+
}
|
|
225
230
|
return client.searchIssues({ jql });
|
|
226
231
|
}
|
|
227
232
|
|
|
@@ -249,6 +254,9 @@ async function fetchIssuesCustom(
|
|
|
249
254
|
|
|
250
255
|
console.log('🔍 Fetching issues (CUSTOM strategy):', jql);
|
|
251
256
|
|
|
257
|
+
if (typeof (client as any).getAxiosClient === 'function') {
|
|
258
|
+
return searchAllIssues((client as any).getAxiosClient(), { jql });
|
|
259
|
+
}
|
|
252
260
|
return client.searchIssues({ jql });
|
|
253
261
|
}
|
|
254
262
|
|
|
@@ -279,5 +287,8 @@ async function fetchIssuesFiltered(
|
|
|
279
287
|
|
|
280
288
|
console.log('🔍 Fetching issues (FILTERED strategy):', jql);
|
|
281
289
|
|
|
290
|
+
if (typeof (client as any).getAxiosClient === 'function') {
|
|
291
|
+
return searchAllIssues((client as any).getAxiosClient(), { jql });
|
|
292
|
+
}
|
|
282
293
|
return client.searchIssues({ jql });
|
|
283
294
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
mapUserStoryToProjects
|
|
6
6
|
} from "../../../src/utils/project-mapper.js";
|
|
7
7
|
import { parseSpecFile } from "../../../src/utils/spec-splitter.js";
|
|
8
|
+
import { getEpicLinkFieldForProject } from "./jira-field-discovery.js";
|
|
8
9
|
class JiraMultiProjectSync {
|
|
9
10
|
constructor(config) {
|
|
10
11
|
this.config = config;
|
|
@@ -43,14 +44,6 @@ Please verify project keys and access permissions.`
|
|
|
43
44
|
const results = [];
|
|
44
45
|
await this.validateProjects();
|
|
45
46
|
const parsedSpec = await parseSpecFile(specPath);
|
|
46
|
-
const epicsByProject = /* @__PURE__ */ new Map();
|
|
47
|
-
if (this.config.autoCreateEpics !== false) {
|
|
48
|
-
for (const project of this.config.projects) {
|
|
49
|
-
const epicResult = await this.createEpicForProject(parsedSpec, project);
|
|
50
|
-
epicsByProject.set(project, epicResult.issueKey);
|
|
51
|
-
results.push(epicResult);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
47
|
const projectStories = /* @__PURE__ */ new Map();
|
|
55
48
|
for (const userStory of parsedSpec.userStories) {
|
|
56
49
|
if (this.config.intelligentMapping !== false) {
|
|
@@ -85,6 +78,14 @@ Please verify project keys and access permissions.`
|
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
}
|
|
81
|
+
const epicsByProject = /* @__PURE__ */ new Map();
|
|
82
|
+
if (this.config.autoCreateEpics !== false) {
|
|
83
|
+
for (const projectId of projectStories.keys()) {
|
|
84
|
+
const epicResult = await this.createEpicForProject(parsedSpec, projectId);
|
|
85
|
+
epicsByProject.set(projectId, epicResult.issueKey);
|
|
86
|
+
results.push(epicResult);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
88
89
|
for (const [projectId, stories] of projectStories.entries()) {
|
|
89
90
|
const epicKey = epicsByProject.get(projectId);
|
|
90
91
|
for (const { story, confidence } of stories) {
|
|
@@ -167,7 +168,16 @@ _Classification confidence: ${(confidence * 100).toFixed(0)}%_
|
|
|
167
168
|
issuetype: { name: this.getIssueTypeName(itemType) }
|
|
168
169
|
};
|
|
169
170
|
if (epicKey) {
|
|
170
|
-
|
|
171
|
+
const { field: epicField, style } = await getEpicLinkFieldForProject(
|
|
172
|
+
this.config.domain,
|
|
173
|
+
projectId,
|
|
174
|
+
{ email: this.config.email, apiToken: this.config.apiToken }
|
|
175
|
+
);
|
|
176
|
+
if (style === "next-gen") {
|
|
177
|
+
issueData.parent = { key: epicKey };
|
|
178
|
+
} else {
|
|
179
|
+
issueData[epicField] = epicKey;
|
|
180
|
+
}
|
|
171
181
|
}
|
|
172
182
|
const issue = await this.client.createIssue(issueData);
|
|
173
183
|
return {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
mapUserStoryToProjects
|
|
24
24
|
} from '../../../src/utils/project-mapper.js';
|
|
25
25
|
import { parseSpecFile } from '../../../src/utils/spec-splitter.js';
|
|
26
|
+
import { getEpicLinkFieldForProject } from './jira-field-discovery.js';
|
|
26
27
|
|
|
27
28
|
export interface JiraMultiProjectConfig {
|
|
28
29
|
domain: string;
|
|
@@ -108,18 +109,7 @@ export class JiraMultiProjectSync {
|
|
|
108
109
|
// Parse spec
|
|
109
110
|
const parsedSpec = await parseSpecFile(specPath);
|
|
110
111
|
|
|
111
|
-
// Step 1:
|
|
112
|
-
const epicsByProject = new Map<string, string>(); // projectId → epicKey
|
|
113
|
-
|
|
114
|
-
if (this.config.autoCreateEpics !== false) {
|
|
115
|
-
for (const project of this.config.projects) {
|
|
116
|
-
const epicResult = await this.createEpicForProject(parsedSpec, project);
|
|
117
|
-
epicsByProject.set(project, epicResult.issueKey);
|
|
118
|
-
results.push(epicResult);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Step 2: Classify user stories by project
|
|
112
|
+
// Step 1: Classify user stories by project FIRST (before creating epics)
|
|
123
113
|
const projectStories = new Map<string, Array<{ story: UserStory; confidence: number }>>();
|
|
124
114
|
|
|
125
115
|
for (const userStory of parsedSpec.userStories) {
|
|
@@ -162,6 +152,17 @@ export class JiraMultiProjectSync {
|
|
|
162
152
|
}
|
|
163
153
|
}
|
|
164
154
|
|
|
155
|
+
// Step 2: Create epics ONLY for projects that have classified stories
|
|
156
|
+
const epicsByProject = new Map<string, string>(); // projectId → epicKey
|
|
157
|
+
|
|
158
|
+
if (this.config.autoCreateEpics !== false) {
|
|
159
|
+
for (const projectId of projectStories.keys()) {
|
|
160
|
+
const epicResult = await this.createEpicForProject(parsedSpec, projectId);
|
|
161
|
+
epicsByProject.set(projectId, epicResult.issueKey);
|
|
162
|
+
results.push(epicResult);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
165
166
|
// Step 3: Create issues in each project
|
|
166
167
|
for (const [projectId, stories] of projectStories.entries()) {
|
|
167
168
|
const epicKey = epicsByProject.get(projectId);
|
|
@@ -255,9 +256,19 @@ ${confidence !== undefined ? `\n_Classification confidence: ${(confidence * 100)
|
|
|
255
256
|
issuetype: { name: this.getIssueTypeName(itemType) }
|
|
256
257
|
};
|
|
257
258
|
|
|
258
|
-
// Link to epic
|
|
259
|
+
// Link to epic using dynamic field discovery
|
|
259
260
|
if (epicKey) {
|
|
260
|
-
|
|
261
|
+
const { field: epicField, style } = await getEpicLinkFieldForProject(
|
|
262
|
+
this.config.domain,
|
|
263
|
+
projectId,
|
|
264
|
+
{ email: this.config.email, apiToken: this.config.apiToken }
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (style === 'next-gen') {
|
|
268
|
+
issueData.parent = { key: epicKey };
|
|
269
|
+
} else {
|
|
270
|
+
issueData[epicField] = epicKey;
|
|
271
|
+
}
|
|
261
272
|
}
|
|
262
273
|
|
|
263
274
|
const issue = await this.client.createIssue(issueData);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
2
|
+
const MAX_RETRIES = 3;
|
|
3
|
+
const BASE_DELAY_MS = 1e3;
|
|
4
|
+
async function searchAllIssues(client, options) {
|
|
5
|
+
const { jql, fields, maxResults = DEFAULT_PAGE_SIZE } = options;
|
|
6
|
+
const allIssues = [];
|
|
7
|
+
let startAt = 0;
|
|
8
|
+
let total = Infinity;
|
|
9
|
+
while (startAt < total) {
|
|
10
|
+
const response = await requestWithRetry(client, "/search", {
|
|
11
|
+
params: {
|
|
12
|
+
jql,
|
|
13
|
+
startAt,
|
|
14
|
+
maxResults,
|
|
15
|
+
...fields ? { fields } : {}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const data = response.data;
|
|
19
|
+
total = data.total;
|
|
20
|
+
const issues = data.issues || [];
|
|
21
|
+
allIssues.push(...issues);
|
|
22
|
+
startAt += issues.length;
|
|
23
|
+
if (issues.length === 0) break;
|
|
24
|
+
}
|
|
25
|
+
return allIssues;
|
|
26
|
+
}
|
|
27
|
+
async function requestWithRetry(client, url, config, attempt = 0) {
|
|
28
|
+
try {
|
|
29
|
+
return await client.get(url, config);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const axiosError = error;
|
|
32
|
+
const status = axiosError.response?.status;
|
|
33
|
+
if (status === 429 && attempt < MAX_RETRIES) {
|
|
34
|
+
const retryAfterHeader = axiosError.response?.headers?.["retry-after"];
|
|
35
|
+
const retryAfterMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : BASE_DELAY_MS * Math.pow(2, attempt);
|
|
36
|
+
console.warn(
|
|
37
|
+
`Rate limited (429). Retry ${attempt + 1}/${MAX_RETRIES} after ${retryAfterMs}ms...`
|
|
38
|
+
);
|
|
39
|
+
await sleep(retryAfterMs);
|
|
40
|
+
return requestWithRetry(client, url, config, attempt + 1);
|
|
41
|
+
}
|
|
42
|
+
if (status === 429) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`JIRA rate limit exceeded after ${MAX_RETRIES} retries. Try again later or reduce request frequency.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function sleep(ms) {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
searchAllIssues
|
|
55
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA Paginated Search with Rate-Limit Retry
|
|
3
|
+
*
|
|
4
|
+
* Provides a paginated JQL search that fetches all results
|
|
5
|
+
* by iterating through pages using startAt/maxResults.
|
|
6
|
+
*
|
|
7
|
+
* Includes exponential backoff retry on HTTP 429 (rate limit).
|
|
8
|
+
*
|
|
9
|
+
* @module jira-paginated-search
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
15
|
+
const MAX_RETRIES = 3;
|
|
16
|
+
const BASE_DELAY_MS = 1000;
|
|
17
|
+
|
|
18
|
+
export interface PaginatedSearchOptions {
|
|
19
|
+
jql: string;
|
|
20
|
+
fields?: string;
|
|
21
|
+
maxResults?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Search all issues matching a JQL query with full pagination.
|
|
26
|
+
* Handles rate limiting with exponential backoff.
|
|
27
|
+
*
|
|
28
|
+
* @param client - Axios instance configured with JIRA auth
|
|
29
|
+
* @param options - Search options (jql, fields, maxResults per page)
|
|
30
|
+
* @returns All matching issues across all pages
|
|
31
|
+
*/
|
|
32
|
+
export async function searchAllIssues(
|
|
33
|
+
client: AxiosInstance,
|
|
34
|
+
options: PaginatedSearchOptions
|
|
35
|
+
): Promise<any[]> {
|
|
36
|
+
const { jql, fields, maxResults = DEFAULT_PAGE_SIZE } = options;
|
|
37
|
+
const allIssues: any[] = [];
|
|
38
|
+
let startAt = 0;
|
|
39
|
+
let total = Infinity;
|
|
40
|
+
|
|
41
|
+
while (startAt < total) {
|
|
42
|
+
const response = await requestWithRetry(client, '/search', {
|
|
43
|
+
params: {
|
|
44
|
+
jql,
|
|
45
|
+
startAt,
|
|
46
|
+
maxResults,
|
|
47
|
+
...(fields ? { fields } : {}),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const data = response.data;
|
|
52
|
+
total = data.total;
|
|
53
|
+
const issues = data.issues || [];
|
|
54
|
+
allIssues.push(...issues);
|
|
55
|
+
|
|
56
|
+
startAt += issues.length;
|
|
57
|
+
|
|
58
|
+
// Safety: if no issues returned, break to avoid infinite loop
|
|
59
|
+
if (issues.length === 0) break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return allIssues;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Make an HTTP GET request with exponential backoff retry on 429.
|
|
67
|
+
*/
|
|
68
|
+
async function requestWithRetry(
|
|
69
|
+
client: AxiosInstance,
|
|
70
|
+
url: string,
|
|
71
|
+
config: any,
|
|
72
|
+
attempt: number = 0
|
|
73
|
+
): Promise<any> {
|
|
74
|
+
try {
|
|
75
|
+
return await client.get(url, config);
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
const axiosError = error as AxiosError;
|
|
78
|
+
const status = axiosError.response?.status;
|
|
79
|
+
|
|
80
|
+
if (status === 429 && attempt < MAX_RETRIES) {
|
|
81
|
+
// Read Retry-After header (seconds) or use exponential backoff
|
|
82
|
+
const retryAfterHeader = axiosError.response?.headers?.['retry-after'];
|
|
83
|
+
const retryAfterMs = retryAfterHeader
|
|
84
|
+
? parseInt(retryAfterHeader, 10) * 1000
|
|
85
|
+
: BASE_DELAY_MS * Math.pow(2, attempt);
|
|
86
|
+
|
|
87
|
+
console.warn(
|
|
88
|
+
`Rate limited (429). Retry ${attempt + 1}/${MAX_RETRIES} after ${retryAfterMs}ms...`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await sleep(retryAfterMs);
|
|
92
|
+
return requestWithRetry(client, url, config, attempt + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (status === 429) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`JIRA rate limit exceeded after ${MAX_RETRIES} retries. ` +
|
|
98
|
+
`Try again later or reduce request frequency.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sleep(ms: number): Promise<void> {
|
|
107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
}
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
generateSmartSummary,
|
|
19
19
|
formatForJira
|
|
20
20
|
} from "../../../src/core/comment-builder.js";
|
|
21
|
+
import { readIssueKey } from "./metadata-paths.js";
|
|
22
|
+
import { getApiBaseUrl } from "./jira-deployment-detector.js";
|
|
21
23
|
import path from "path";
|
|
22
24
|
import fs from "fs/promises";
|
|
23
25
|
async function syncSpecCommitsToJira(config, options) {
|
|
@@ -38,7 +40,7 @@ async function syncSpecCommitsToJira(config, options) {
|
|
|
38
40
|
result.errors.push("No metadata.json found");
|
|
39
41
|
return result;
|
|
40
42
|
}
|
|
41
|
-
const jiraIssueKey = metadata
|
|
43
|
+
const jiraIssueKey = readIssueKey(metadata);
|
|
42
44
|
if (!jiraIssueKey) {
|
|
43
45
|
if (verbose) {
|
|
44
46
|
console.log("No JIRA issue linked to increment");
|
|
@@ -46,7 +48,7 @@ async function syncSpecCommitsToJira(config, options) {
|
|
|
46
48
|
return result;
|
|
47
49
|
}
|
|
48
50
|
const client = axios.create({
|
|
49
|
-
baseURL:
|
|
51
|
+
baseURL: getApiBaseUrl(config.domain),
|
|
50
52
|
auth: {
|
|
51
53
|
username: config.email,
|
|
52
54
|
password: config.apiToken
|
|
@@ -194,7 +196,7 @@ Short update comment (JIRA format):`);
|
|
|
194
196
|
async function postCommitBatchUpdate(config, incrementPath, commits, issueKey, repo, dryRun = false) {
|
|
195
197
|
try {
|
|
196
198
|
const client = axios.create({
|
|
197
|
-
baseURL:
|
|
199
|
+
baseURL: getApiBaseUrl(config.domain),
|
|
198
200
|
auth: {
|
|
199
201
|
username: config.email,
|
|
200
202
|
password: config.apiToken
|