@sk8metal/michi-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/scripts/config-global.d.ts +10 -0
- package/dist/scripts/config-global.d.ts.map +1 -0
- package/dist/scripts/config-global.js +111 -0
- package/dist/scripts/config-global.js.map +1 -0
- package/dist/scripts/confluence-sync.d.ts +22 -4
- package/dist/scripts/confluence-sync.d.ts.map +1 -1
- package/dist/scripts/confluence-sync.js +22 -12
- package/dist/scripts/confluence-sync.js.map +1 -1
- package/dist/scripts/jira-sync.d.ts.map +1 -1
- package/dist/scripts/jira-sync.js +201 -167
- package/dist/scripts/jira-sync.js.map +1 -1
- package/dist/scripts/list-projects.js.map +1 -1
- package/dist/scripts/multi-project-estimate.js.map +1 -1
- package/dist/scripts/phase-runner.d.ts +1 -1
- package/dist/scripts/phase-runner.d.ts.map +1 -1
- package/dist/scripts/phase-runner.js +295 -522
- package/dist/scripts/phase-runner.js.map +1 -1
- package/dist/scripts/pre-flight-check.d.ts.map +1 -1
- package/dist/scripts/pre-flight-check.js +10 -6
- package/dist/scripts/pre-flight-check.js.map +1 -1
- package/dist/scripts/resource-dashboard.js.map +1 -1
- package/dist/scripts/spec-impl-workflow.js +1 -1
- package/dist/scripts/spec-impl-workflow.js.map +1 -1
- package/dist/scripts/template/renderer.d.ts +1 -1
- package/dist/scripts/template/renderer.d.ts.map +1 -1
- package/dist/scripts/test-interactive.d.ts.map +1 -1
- package/dist/scripts/test-interactive.js +0 -15
- package/dist/scripts/test-interactive.js.map +1 -1
- package/dist/scripts/test-new-features.js +6 -3
- package/dist/scripts/test-new-features.js.map +1 -1
- package/dist/scripts/test-spec-generator.d.ts.map +1 -1
- package/dist/scripts/test-spec-generator.js +1 -2
- package/dist/scripts/test-spec-generator.js.map +1 -1
- package/dist/scripts/utils/config-loader.d.ts +7 -2
- package/dist/scripts/utils/config-loader.d.ts.map +1 -1
- package/dist/scripts/utils/config-loader.js +79 -8
- package/dist/scripts/utils/config-loader.js.map +1 -1
- package/dist/scripts/utils/config-sections.d.ts +54 -0
- package/dist/scripts/utils/config-sections.d.ts.map +1 -0
- package/dist/scripts/utils/config-sections.js +178 -0
- package/dist/scripts/utils/config-sections.js.map +1 -0
- package/dist/scripts/utils/config-validator.d.ts +4 -0
- package/dist/scripts/utils/config-validator.d.ts.map +1 -1
- package/dist/scripts/utils/config-validator.js +57 -1
- package/dist/scripts/utils/config-validator.js.map +1 -1
- package/dist/scripts/utils/confluence-approval.d.ts.map +1 -1
- package/dist/scripts/utils/confluence-approval.js +5 -3
- package/dist/scripts/utils/confluence-approval.js.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
- package/dist/scripts/utils/interactive-helpers.d.ts +32 -0
- package/dist/scripts/utils/interactive-helpers.d.ts.map +1 -0
- package/dist/scripts/utils/interactive-helpers.js +92 -0
- package/dist/scripts/utils/interactive-helpers.js.map +1 -0
- package/dist/scripts/utils/jira-issue-type-fetcher.d.ts.map +1 -1
- package/dist/scripts/utils/jira-issue-type-fetcher.js +27 -18
- package/dist/scripts/utils/jira-issue-type-fetcher.js.map +1 -1
- package/dist/scripts/utils/release-notes-generator.d.ts.map +1 -1
- package/dist/scripts/utils/release-notes-generator.js +2 -1
- package/dist/scripts/utils/release-notes-generator.js.map +1 -1
- package/dist/scripts/utils/spec-updater.d.ts +19 -0
- package/dist/scripts/utils/spec-updater.d.ts.map +1 -1
- package/dist/scripts/utils/spec-updater.js.map +1 -1
- package/dist/scripts/utils/tasks-converter.d.ts.map +1 -1
- package/dist/scripts/utils/tasks-converter.js +2 -2
- package/dist/scripts/utils/tasks-converter.js.map +1 -1
- package/dist/scripts/utils/tasks-format-validator.d.ts.map +1 -1
- package/dist/scripts/utils/tasks-format-validator.js +0 -12
- package/dist/scripts/utils/tasks-format-validator.js.map +1 -1
- package/dist/scripts/utils/test-runner.d.ts.map +1 -1
- package/dist/scripts/utils/test-runner.js +3 -2
- package/dist/scripts/utils/test-runner.js.map +1 -1
- package/dist/scripts/validate-phase.d.ts +1 -1
- package/dist/scripts/validate-phase.d.ts.map +1 -1
- package/dist/scripts/validate-phase.js +12 -62
- package/dist/scripts/validate-phase.js.map +1 -1
- package/dist/scripts/workflow-orchestrator.d.ts.map +1 -1
- package/dist/scripts/workflow-orchestrator.js +11 -16
- package/dist/scripts/workflow-orchestrator.js.map +1 -1
- package/dist/src/__tests__/integration/setup/init.test.d.ts +5 -0
- package/dist/src/__tests__/integration/setup/init.test.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/init.test.js +352 -0
- package/dist/src/__tests__/integration/setup/init.test.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +28 -20
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/init.d.ts +28 -0
- package/dist/src/commands/init.d.ts.map +1 -0
- package/dist/src/commands/init.js +490 -0
- package/dist/src/commands/init.js.map +1 -0
- package/docs/user-guide/getting-started/setup.md +31 -3
- package/docs/user-guide/guides/customization.md +64 -11
- package/docs/user-guide/guides/workflow.md +35 -21
- package/docs/user-guide/reference/config.md +30 -5
- package/docs/user-guide/reference/quick-reference.md +68 -74
- package/docs/user-guide/testing/test-planning-flow.md +4 -0
- package/package.json +2 -4
- package/scripts/config-global.ts +160 -0
- package/scripts/confluence-sync.ts +91 -27
- package/scripts/jira-sync.ts +284 -218
- package/scripts/list-projects.ts +2 -2
- package/scripts/multi-project-estimate.ts +3 -3
- package/scripts/phase-runner.ts +391 -594
- package/scripts/pre-flight-check.ts +20 -9
- package/scripts/pre-publish-check.sh +3 -34
- package/scripts/resource-dashboard.ts +4 -4
- package/scripts/spec-impl-workflow.ts +1 -1
- package/scripts/template/renderer.ts +1 -1
- package/scripts/test-interactive.ts +0 -19
- package/scripts/test-new-features.ts +10 -7
- package/scripts/test-npm-package.sh +3 -34
- package/scripts/test-spec-generator.ts +3 -7
- package/scripts/utils/config-loader.ts +107 -26
- package/scripts/utils/config-sections.ts +316 -0
- package/scripts/utils/config-validator.ts +66 -1
- package/scripts/utils/confluence-approval.ts +8 -6
- package/scripts/utils/confluence-hierarchy.ts +27 -27
- package/scripts/utils/interactive-helpers.ts +135 -0
- package/scripts/utils/jira-issue-type-fetcher.ts +29 -21
- package/scripts/utils/release-notes-generator.ts +3 -2
- package/scripts/utils/spec-updater.ts +37 -15
- package/scripts/utils/tasks-converter.ts +4 -6
- package/scripts/utils/tasks-format-validator.ts +0 -13
- package/scripts/utils/test-runner.ts +4 -3
- package/scripts/validate-phase.ts +21 -80
- package/scripts/workflow-orchestrator.ts +16 -25
- package/templates/claude/commands/kiro/kiro-spec-impl.md +4 -0
- package/templates/claude/commands/kiro/kiro-spec-tasks.md +3 -1
- package/templates/claude/commands/michi/confluence-sync.md +8 -2
- package/templates/claude/commands/michi/design-review.md +4 -0
- package/templates/claude/commands/michi/e2e-plan.md +4 -0
- package/templates/claude/commands/michi/license-check.md +4 -0
- package/templates/claude/commands/michi/pr-resolve.md +4 -0
- package/templates/claude/commands/michi/project-switch.md +8 -2
- package/templates/claude/commands/michi/spec-design.md +78 -0
- package/templates/claude/commands/michi/spec-impl.md +716 -0
- package/templates/claude/commands/michi/test-planning.md +174 -0
- package/templates/claude/commands/michi/validate-design.md +58 -0
- package/templates/claude/commands/michi/version-audit.md +4 -0
- package/templates/michi/cc-sdd-overrides/README.md +8 -0
- package/templates/michi/cc-sdd-overrides/settings/rules/design-review-michi.md +53 -0
- package/dist/scripts/config-interactive.d.ts +0 -10
- package/dist/scripts/config-interactive.d.ts.map +0 -1
- package/dist/scripts/config-interactive.js +0 -372
- package/dist/scripts/config-interactive.js.map +0 -1
- package/dist/scripts/setup-existing-project.d.ts +0 -15
- package/dist/scripts/setup-existing-project.d.ts.map +0 -1
- package/dist/scripts/setup-existing-project.js +0 -455
- package/dist/scripts/setup-existing-project.js.map +0 -1
- package/dist/scripts/setup-interactive.d.ts +0 -10
- package/dist/scripts/setup-interactive.d.ts.map +0 -1
- package/dist/scripts/setup-interactive.js +0 -413
- package/dist/scripts/setup-interactive.js.map +0 -1
- package/scripts/config-interactive.ts +0 -550
- package/scripts/setup-existing-project.ts +0 -585
- package/scripts/setup-interactive.ts +0 -565
package/scripts/jira-sync.ts
CHANGED
|
@@ -32,6 +32,18 @@ import {
|
|
|
32
32
|
|
|
33
33
|
config();
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* JIRA Issue基本型
|
|
37
|
+
*/
|
|
38
|
+
interface JiraIssue {
|
|
39
|
+
id: string;
|
|
40
|
+
key: string;
|
|
41
|
+
fields?: {
|
|
42
|
+
summary?: string;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
/**
|
|
36
48
|
* リクエスト間のスリープ処理(レートリミット対策)
|
|
37
49
|
*/
|
|
@@ -668,6 +680,252 @@ class JIRAClient {
|
|
|
668
680
|
}
|
|
669
681
|
}
|
|
670
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Phase行からフェーズラベルを検出
|
|
685
|
+
* @param line Markdown行
|
|
686
|
+
* @returns フェーズラベル、または検出されない場合はnull
|
|
687
|
+
*/
|
|
688
|
+
function detectPhaseLabel(line: string): string | null {
|
|
689
|
+
const phasePattern = /## Phase [\d.A-Z]+:\s*(.+?)(?:((.+?)))?/;
|
|
690
|
+
const phaseMatch = line.match(phasePattern);
|
|
691
|
+
if (!phaseMatch) return null;
|
|
692
|
+
|
|
693
|
+
const phaseTitle = phaseMatch[1]; // フェーズタイトル全体
|
|
694
|
+
const phaseName = phaseMatch[2] || phaseTitle; // 括弧内のラベル(例: Requirements)または全体
|
|
695
|
+
|
|
696
|
+
// Phase番号を抽出(例: "0.1", "2", "A")
|
|
697
|
+
const phaseNumberMatch = line.match(/## Phase ([\d.A-Z]+):/);
|
|
698
|
+
const phaseNumber = phaseNumberMatch ? phaseNumberMatch[1] : '';
|
|
699
|
+
|
|
700
|
+
// フェーズ番号またはフェーズ名からラベルを決定
|
|
701
|
+
if (
|
|
702
|
+
phaseNumber === '0.0' ||
|
|
703
|
+
phaseName.includes('初期化') ||
|
|
704
|
+
phaseName.toLowerCase().includes('init')
|
|
705
|
+
) {
|
|
706
|
+
return 'spec-init';
|
|
707
|
+
} else if (
|
|
708
|
+
phaseNumber === '0.1' ||
|
|
709
|
+
phaseName.includes('要件定義') ||
|
|
710
|
+
phaseName.toLowerCase().includes('requirements')
|
|
711
|
+
) {
|
|
712
|
+
return 'requirements';
|
|
713
|
+
} else if (
|
|
714
|
+
phaseNumber === '0.2' ||
|
|
715
|
+
phaseName.includes('設計') ||
|
|
716
|
+
phaseName.toLowerCase().includes('design')
|
|
717
|
+
) {
|
|
718
|
+
return 'design';
|
|
719
|
+
} else if (
|
|
720
|
+
phaseNumber === '0.3' ||
|
|
721
|
+
phaseName.includes('テストタイプ') ||
|
|
722
|
+
phaseName.toLowerCase().includes('test-type') ||
|
|
723
|
+
phaseName.toLowerCase().includes('test type')
|
|
724
|
+
) {
|
|
725
|
+
return 'test-type-selection';
|
|
726
|
+
} else if (
|
|
727
|
+
phaseNumber === '0.4' ||
|
|
728
|
+
phaseName.includes('テスト仕様') ||
|
|
729
|
+
phaseName.toLowerCase().includes('test-spec') ||
|
|
730
|
+
phaseName.toLowerCase().includes('test spec')
|
|
731
|
+
) {
|
|
732
|
+
return 'test-spec';
|
|
733
|
+
} else if (
|
|
734
|
+
phaseNumber === '0.5' ||
|
|
735
|
+
phaseName.includes('タスク分割') ||
|
|
736
|
+
phaseName.toLowerCase().includes('tasks') ||
|
|
737
|
+
phaseName.toLowerCase().includes('task breakdown')
|
|
738
|
+
) {
|
|
739
|
+
return 'spec-tasks';
|
|
740
|
+
} else if (
|
|
741
|
+
phaseNumber === '0.6' ||
|
|
742
|
+
phaseName.includes('JIRA') ||
|
|
743
|
+
phaseName.toLowerCase().includes('jira')
|
|
744
|
+
) {
|
|
745
|
+
return 'jira-sync';
|
|
746
|
+
} else if (
|
|
747
|
+
phaseNumber === '1' ||
|
|
748
|
+
phaseName.includes('環境構築') ||
|
|
749
|
+
phaseName.toLowerCase().includes('environment') ||
|
|
750
|
+
phaseName.toLowerCase().includes('setup')
|
|
751
|
+
) {
|
|
752
|
+
return 'environment-setup';
|
|
753
|
+
} else if (
|
|
754
|
+
phaseNumber === '2' ||
|
|
755
|
+
phaseName.includes('実装') ||
|
|
756
|
+
phaseName.includes('TDD') ||
|
|
757
|
+
phaseName.toLowerCase().includes('implementation')
|
|
758
|
+
) {
|
|
759
|
+
return 'implementation';
|
|
760
|
+
} else if (
|
|
761
|
+
phaseNumber === 'A' ||
|
|
762
|
+
phaseNumber.toLowerCase() === 'a' ||
|
|
763
|
+
phaseName.includes('PR前') ||
|
|
764
|
+
phaseName.toLowerCase().includes('pr-test') ||
|
|
765
|
+
phaseName.toLowerCase().includes('pr test')
|
|
766
|
+
) {
|
|
767
|
+
return 'phase-a';
|
|
768
|
+
} else if (
|
|
769
|
+
phaseNumber === '3' ||
|
|
770
|
+
phaseName.includes('追加QA') ||
|
|
771
|
+
phaseName.includes('QA') ||
|
|
772
|
+
phaseName.includes('試験') ||
|
|
773
|
+
phaseName.toLowerCase().includes('testing') ||
|
|
774
|
+
phaseName.toLowerCase().includes('additional qa')
|
|
775
|
+
) {
|
|
776
|
+
return 'additional-qa';
|
|
777
|
+
} else if (
|
|
778
|
+
phaseNumber === 'B' ||
|
|
779
|
+
phaseNumber.toLowerCase() === 'b' ||
|
|
780
|
+
phaseName.includes('リリース準備テスト') ||
|
|
781
|
+
phaseName.toLowerCase().includes('release-test') ||
|
|
782
|
+
phaseName.toLowerCase().includes('release test')
|
|
783
|
+
) {
|
|
784
|
+
return 'phase-b';
|
|
785
|
+
} else if (
|
|
786
|
+
phaseNumber === '4' ||
|
|
787
|
+
phaseName.includes('リリース準備') ||
|
|
788
|
+
phaseName.toLowerCase().includes('release-prep') ||
|
|
789
|
+
phaseName.toLowerCase().includes('release preparation')
|
|
790
|
+
) {
|
|
791
|
+
return 'release-prep';
|
|
792
|
+
} else if (
|
|
793
|
+
phaseNumber === '5' ||
|
|
794
|
+
(phaseName.includes('リリース') && !phaseName.includes('準備')) ||
|
|
795
|
+
(phaseName.toLowerCase().includes('release') &&
|
|
796
|
+
!phaseName.toLowerCase().includes('prep'))
|
|
797
|
+
) {
|
|
798
|
+
return 'release';
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Story Issue Type IDを取得
|
|
806
|
+
* @param appConfig アプリケーション設定
|
|
807
|
+
* @param projectMeta プロジェクトメタデータ
|
|
808
|
+
* @param client JIRAクライアント
|
|
809
|
+
* @returns Story Issue Type ID
|
|
810
|
+
*/
|
|
811
|
+
async function getStoryIssueTypeId(
|
|
812
|
+
appConfig: { jira?: { issueTypes?: { story?: string | null } } & Record<string, unknown> } & Record<string, unknown>,
|
|
813
|
+
projectMeta: { jiraProjectKey: string },
|
|
814
|
+
client: JIRAClient,
|
|
815
|
+
): Promise<string> {
|
|
816
|
+
// StoryタイプのIDを動的に取得(日本語JIRAでは "ストーリー" という名前の場合がある)
|
|
817
|
+
let storyIssueTypeId: string | undefined =
|
|
818
|
+
appConfig.jira?.issueTypes?.story || process.env.JIRA_ISSUE_TYPE_STORY;
|
|
819
|
+
console.log(
|
|
820
|
+
`📋 Story Issue Type ID from config/env: ${storyIssueTypeId || 'not found'}`,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
if (!storyIssueTypeId) {
|
|
824
|
+
console.log('🔍 Attempting to find Story issue type dynamically...');
|
|
825
|
+
const foundId =
|
|
826
|
+
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'Story')) ||
|
|
827
|
+
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'ストーリー'));
|
|
828
|
+
storyIssueTypeId = foundId ?? undefined;
|
|
829
|
+
console.log(
|
|
830
|
+
`📋 Story Issue Type ID from API: ${storyIssueTypeId || 'not found'}`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!storyIssueTypeId) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
'JIRA Story issue type ID is not configured and could not be found in project. ' +
|
|
837
|
+
'Please set JIRA_ISSUE_TYPE_STORY environment variable or configure it in .michi/config.json. ' +
|
|
838
|
+
'You can find the issue type ID in JIRA UI (Settings > Issues > Issue types) or via REST API: ' +
|
|
839
|
+
'GET https://your-domain.atlassian.net/rest/api/3/project/{projectKey}',
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
console.log(`✅ Using Story Issue Type ID: ${storyIssueTypeId}`);
|
|
844
|
+
return storyIssueTypeId;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Epic を取得または作成
|
|
849
|
+
* @param featureName 機能名
|
|
850
|
+
* @param spec spec.json の内容
|
|
851
|
+
* @param projectMeta プロジェクトメタデータ
|
|
852
|
+
* @param client JIRAクライアント
|
|
853
|
+
* @returns Epic キー
|
|
854
|
+
*/
|
|
855
|
+
async function getOrCreateEpic(
|
|
856
|
+
featureName: string,
|
|
857
|
+
spec: SpecJson,
|
|
858
|
+
projectMeta: { projectName: string; jiraProjectKey: string; repository: string; confluenceLabels: string[] },
|
|
859
|
+
client: JIRAClient,
|
|
860
|
+
): Promise<{ key: string }> {
|
|
861
|
+
// 既存のEpicをチェック
|
|
862
|
+
if (spec.jira?.epicKey) {
|
|
863
|
+
console.log(`Existing Epic found: ${spec.jira.epicKey}`);
|
|
864
|
+
console.log('Skipping Epic creation (already exists)');
|
|
865
|
+
return { key: spec.jira.epicKey };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Epic作成
|
|
869
|
+
console.log('Creating Epic...');
|
|
870
|
+
const epicSummary = `[${featureName}] ${projectMeta.projectName}`;
|
|
871
|
+
|
|
872
|
+
// 同じタイトルのEpicがすでに存在するかJQLで検索
|
|
873
|
+
const jql = `project = ${projectMeta.jiraProjectKey} AND issuetype = Epic AND summary ~ "${featureName}"`;
|
|
874
|
+
let existingEpics: JIRAIssue[] = [];
|
|
875
|
+
try {
|
|
876
|
+
existingEpics = await client.searchIssues(jql);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
console.error(
|
|
879
|
+
'❌ Failed to search existing Epics:',
|
|
880
|
+
error instanceof Error ? error.message : error,
|
|
881
|
+
);
|
|
882
|
+
console.error(
|
|
883
|
+
'⚠️ Cannot verify idempotency - proceeding with Epic creation',
|
|
884
|
+
);
|
|
885
|
+
console.error(
|
|
886
|
+
' If Epic already exists, manual cleanup may be required',
|
|
887
|
+
);
|
|
888
|
+
// 検索失敗時はフォールバック: 新規作成を試みる(重複リスクあり)
|
|
889
|
+
existingEpics = [];
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (existingEpics.length > 0) {
|
|
893
|
+
console.log(
|
|
894
|
+
`Found existing Epic with similar title: ${existingEpics[0].key}`,
|
|
895
|
+
);
|
|
896
|
+
console.log('Using existing Epic instead of creating new one');
|
|
897
|
+
return existingEpics[0];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// EpicタイプのIDを取得(日本語JIRAでは "エピック" という名前の場合がある)
|
|
901
|
+
const epicTypeId =
|
|
902
|
+
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'Epic')) ||
|
|
903
|
+
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'エピック'));
|
|
904
|
+
|
|
905
|
+
if (!epicTypeId) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
'Epic issue type not found in project. ' +
|
|
908
|
+
'Please ensure the project has Epic issue type enabled.',
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const epicDescription = `機能: ${featureName}\nGitHub: ${projectMeta.repository}/tree/main/.kiro/specs/${featureName}`;
|
|
913
|
+
|
|
914
|
+
const epicPayload = {
|
|
915
|
+
fields: {
|
|
916
|
+
project: { key: projectMeta.jiraProjectKey },
|
|
917
|
+
summary: epicSummary,
|
|
918
|
+
description: textToADF(epicDescription), // ADF形式に変換
|
|
919
|
+
issuetype: { id: epicTypeId }, // IDを使用(nameではなく)
|
|
920
|
+
labels: projectMeta.confluenceLabels,
|
|
921
|
+
},
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const epic = await client.createIssue(epicPayload);
|
|
925
|
+
console.log(`✅ Epic created: ${epic.key}`);
|
|
926
|
+
return epic;
|
|
927
|
+
}
|
|
928
|
+
|
|
671
929
|
async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
672
930
|
console.log(`Syncing tasks for feature: ${featureName}`);
|
|
673
931
|
|
|
@@ -706,35 +964,13 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
706
964
|
const config = getJIRAConfig();
|
|
707
965
|
const client = new JIRAClient(config);
|
|
708
966
|
|
|
709
|
-
// StoryタイプのID
|
|
710
|
-
|
|
711
|
-
appConfig
|
|
712
|
-
|
|
713
|
-
|
|
967
|
+
// StoryタイプのIDを取得
|
|
968
|
+
const storyIssueTypeId = await getStoryIssueTypeId(
|
|
969
|
+
appConfig,
|
|
970
|
+
projectMeta,
|
|
971
|
+
client,
|
|
714
972
|
);
|
|
715
973
|
|
|
716
|
-
if (!storyIssueTypeId) {
|
|
717
|
-
console.log('🔍 Attempting to find Story issue type dynamically...');
|
|
718
|
-
const foundId =
|
|
719
|
-
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'Story')) ||
|
|
720
|
-
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'ストーリー'));
|
|
721
|
-
storyIssueTypeId = foundId ?? undefined;
|
|
722
|
-
console.log(
|
|
723
|
-
`📋 Story Issue Type ID from API: ${storyIssueTypeId || 'not found'}`,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (!storyIssueTypeId) {
|
|
728
|
-
throw new Error(
|
|
729
|
-
'JIRA Story issue type ID is not configured and could not be found in project. ' +
|
|
730
|
-
'Please set JIRA_ISSUE_TYPE_STORY environment variable or configure it in .michi/config.json. ' +
|
|
731
|
-
'You can find the issue type ID in JIRA UI (Settings > Issues > Issue types) or via REST API: ' +
|
|
732
|
-
'GET https://your-domain.atlassian.net/rest/api/3/project/{projectKey}',
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
console.log(`✅ Using Story Issue Type ID: ${storyIssueTypeId}`);
|
|
737
|
-
|
|
738
974
|
const tasksPath = resolve(`.kiro/specs/${featureName}/tasks.md`);
|
|
739
975
|
const tasksContent = readFileSync(tasksPath, 'utf-8');
|
|
740
976
|
|
|
@@ -747,84 +983,14 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
747
983
|
console.error('spec.json not found or invalid');
|
|
748
984
|
}
|
|
749
985
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
// 既存のEpicをチェック
|
|
753
|
-
if (spec.jira?.epicKey) {
|
|
754
|
-
console.log(`Existing Epic found: ${spec.jira.epicKey}`);
|
|
755
|
-
console.log('Skipping Epic creation (already exists)');
|
|
756
|
-
epic = { key: spec.jira.epicKey };
|
|
757
|
-
} else {
|
|
758
|
-
// Epic作成
|
|
759
|
-
console.log('Creating Epic...');
|
|
760
|
-
const epicSummary = `[${featureName}] ${projectMeta.projectName}`;
|
|
761
|
-
|
|
762
|
-
// 同じタイトルのEpicがすでに存在するかJQLで検索
|
|
763
|
-
const jql = `project = ${projectMeta.jiraProjectKey} AND issuetype = Epic AND summary ~ "${featureName}"`;
|
|
764
|
-
let existingEpics: JIRAIssue[] = [];
|
|
765
|
-
try {
|
|
766
|
-
existingEpics = await client.searchIssues(jql);
|
|
767
|
-
} catch (error) {
|
|
768
|
-
console.error(
|
|
769
|
-
'❌ Failed to search existing Epics:',
|
|
770
|
-
error instanceof Error ? error.message : error,
|
|
771
|
-
);
|
|
772
|
-
console.error(
|
|
773
|
-
'⚠️ Cannot verify idempotency - proceeding with Epic creation',
|
|
774
|
-
);
|
|
775
|
-
console.error(
|
|
776
|
-
' If Epic already exists, manual cleanup may be required',
|
|
777
|
-
);
|
|
778
|
-
// 検索失敗時はフォールバック: 新規作成を試みる(重複リスクあり)
|
|
779
|
-
existingEpics = [];
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
if (existingEpics.length > 0) {
|
|
783
|
-
console.log(
|
|
784
|
-
`Found existing Epic with similar title: ${existingEpics[0].key}`,
|
|
785
|
-
);
|
|
786
|
-
console.log('Using existing Epic instead of creating new one');
|
|
787
|
-
epic = existingEpics[0];
|
|
788
|
-
} else {
|
|
789
|
-
// EpicタイプのIDを取得(日本語JIRAでは "エピック" という名前の場合がある)
|
|
790
|
-
const epicTypeId =
|
|
791
|
-
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'Epic')) ||
|
|
792
|
-
(await client.getIssueTypeId(projectMeta.jiraProjectKey, 'エピック'));
|
|
793
|
-
|
|
794
|
-
if (!epicTypeId) {
|
|
795
|
-
throw new Error(
|
|
796
|
-
'Epic issue type not found in project. ' +
|
|
797
|
-
'Please ensure the project has Epic issue type enabled.',
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const epicDescription = `機能: ${featureName}\nGitHub: ${projectMeta.repository}/tree/main/.kiro/specs/${featureName}`;
|
|
802
|
-
|
|
803
|
-
const epicPayload = {
|
|
804
|
-
fields: {
|
|
805
|
-
project: { key: projectMeta.jiraProjectKey },
|
|
806
|
-
summary: epicSummary,
|
|
807
|
-
description: textToADF(epicDescription), // ADF形式に変換
|
|
808
|
-
issuetype: { id: epicTypeId }, // IDを使用(nameではなく)
|
|
809
|
-
labels: projectMeta.confluenceLabels,
|
|
810
|
-
},
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
epic = await client.createIssue(epicPayload);
|
|
814
|
-
console.log(`✅ Epic created: ${epic.key}`);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Epicが確実に設定されていることを確認
|
|
819
|
-
if (!epic) {
|
|
820
|
-
throw new Error('Epic creation or retrieval failed');
|
|
821
|
-
}
|
|
986
|
+
// Epic を取得または作成
|
|
987
|
+
const epic = await getOrCreateEpic(featureName, spec, projectMeta, client);
|
|
822
988
|
|
|
823
989
|
// 既存のStoryを検索(重複防止)
|
|
824
990
|
// ラベルで検索(summary検索では "Story: タイトル" 形式に一致しないため)
|
|
825
991
|
// issuetype検索にはIDを使用(名前は言語依存のため)
|
|
826
992
|
const storyJql = `project = ${projectMeta.jiraProjectKey} AND issuetype = ${storyIssueTypeId} AND labels = "${featureName}"`;
|
|
827
|
-
let existingStories:
|
|
993
|
+
let existingStories: JiraIssue[] = [];
|
|
828
994
|
try {
|
|
829
995
|
existingStories = await client.searchIssues(storyJql);
|
|
830
996
|
} catch (error) {
|
|
@@ -844,13 +1010,13 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
844
1010
|
|
|
845
1011
|
const existingStorySummaries = new Set(
|
|
846
1012
|
existingStories
|
|
847
|
-
.filter((s:
|
|
848
|
-
.map((s:
|
|
1013
|
+
.filter((s: JiraIssue) => s?.fields?.summary)
|
|
1014
|
+
.map((s: JiraIssue) => s.fields!.summary),
|
|
849
1015
|
);
|
|
850
1016
|
const existingStoryKeys = new Set(
|
|
851
1017
|
existingStories
|
|
852
|
-
.filter((s:
|
|
853
|
-
.map((s:
|
|
1018
|
+
.filter((s: JiraIssue) => s?.key)
|
|
1019
|
+
.map((s: JiraIssue) => s.key),
|
|
854
1020
|
);
|
|
855
1021
|
|
|
856
1022
|
console.log(
|
|
@@ -871,117 +1037,16 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
871
1037
|
const line = lines[i];
|
|
872
1038
|
|
|
873
1039
|
// フェーズ検出
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
const phaseName = phaseMatch[2] || phaseTitle; // 括弧内のラベル(例: Requirements)または全体
|
|
1040
|
+
const detectedPhase = detectPhaseLabel(line);
|
|
1041
|
+
if (detectedPhase) {
|
|
1042
|
+
currentPhaseLabel = detectedPhase;
|
|
878
1043
|
|
|
879
|
-
//
|
|
1044
|
+
// ログ出力用にphaseTitle と phaseNumber を抽出
|
|
1045
|
+
const phaseMatch = line.match(phasePattern);
|
|
1046
|
+
const phaseTitle = phaseMatch ? phaseMatch[1] : '';
|
|
880
1047
|
const phaseNumberMatch = line.match(/## Phase ([\d.A-Z]+):/);
|
|
881
1048
|
const phaseNumber = phaseNumberMatch ? phaseNumberMatch[1] : '';
|
|
882
1049
|
|
|
883
|
-
// フェーズ番号またはフェーズ名からラベルを決定
|
|
884
|
-
// 新ワークフロー構造に対応
|
|
885
|
-
if (
|
|
886
|
-
phaseNumber === '0.0' ||
|
|
887
|
-
phaseName.includes('初期化') ||
|
|
888
|
-
phaseName.toLowerCase().includes('init')
|
|
889
|
-
) {
|
|
890
|
-
currentPhaseLabel = 'spec-init';
|
|
891
|
-
} else if (
|
|
892
|
-
phaseNumber === '0.1' ||
|
|
893
|
-
phaseName.includes('要件定義') ||
|
|
894
|
-
phaseName.toLowerCase().includes('requirements')
|
|
895
|
-
) {
|
|
896
|
-
currentPhaseLabel = 'requirements';
|
|
897
|
-
} else if (
|
|
898
|
-
phaseNumber === '0.2' ||
|
|
899
|
-
phaseName.includes('設計') ||
|
|
900
|
-
phaseName.toLowerCase().includes('design')
|
|
901
|
-
) {
|
|
902
|
-
currentPhaseLabel = 'design';
|
|
903
|
-
} else if (
|
|
904
|
-
phaseNumber === '0.3' ||
|
|
905
|
-
phaseName.includes('テストタイプ') ||
|
|
906
|
-
phaseName.toLowerCase().includes('test-type') ||
|
|
907
|
-
phaseName.toLowerCase().includes('test type')
|
|
908
|
-
) {
|
|
909
|
-
currentPhaseLabel = 'test-type-selection';
|
|
910
|
-
} else if (
|
|
911
|
-
phaseNumber === '0.4' ||
|
|
912
|
-
phaseName.includes('テスト仕様') ||
|
|
913
|
-
phaseName.toLowerCase().includes('test-spec') ||
|
|
914
|
-
phaseName.toLowerCase().includes('test spec')
|
|
915
|
-
) {
|
|
916
|
-
currentPhaseLabel = 'test-spec';
|
|
917
|
-
} else if (
|
|
918
|
-
phaseNumber === '0.5' ||
|
|
919
|
-
phaseName.includes('タスク分割') ||
|
|
920
|
-
phaseName.toLowerCase().includes('tasks') ||
|
|
921
|
-
phaseName.toLowerCase().includes('task breakdown')
|
|
922
|
-
) {
|
|
923
|
-
currentPhaseLabel = 'spec-tasks';
|
|
924
|
-
} else if (
|
|
925
|
-
phaseNumber === '0.6' ||
|
|
926
|
-
phaseName.includes('JIRA') ||
|
|
927
|
-
phaseName.toLowerCase().includes('jira')
|
|
928
|
-
) {
|
|
929
|
-
currentPhaseLabel = 'jira-sync';
|
|
930
|
-
} else if (
|
|
931
|
-
phaseNumber === '1' ||
|
|
932
|
-
phaseName.includes('環境構築') ||
|
|
933
|
-
phaseName.toLowerCase().includes('environment') ||
|
|
934
|
-
phaseName.toLowerCase().includes('setup')
|
|
935
|
-
) {
|
|
936
|
-
currentPhaseLabel = 'environment-setup';
|
|
937
|
-
} else if (
|
|
938
|
-
phaseNumber === '2' ||
|
|
939
|
-
phaseName.includes('実装') ||
|
|
940
|
-
phaseName.includes('TDD') ||
|
|
941
|
-
phaseName.toLowerCase().includes('implementation')
|
|
942
|
-
) {
|
|
943
|
-
currentPhaseLabel = 'implementation';
|
|
944
|
-
} else if (
|
|
945
|
-
phaseNumber === 'A' ||
|
|
946
|
-
phaseNumber.toLowerCase() === 'a' ||
|
|
947
|
-
phaseName.includes('PR前') ||
|
|
948
|
-
phaseName.toLowerCase().includes('pr-test') ||
|
|
949
|
-
phaseName.toLowerCase().includes('pr test')
|
|
950
|
-
) {
|
|
951
|
-
currentPhaseLabel = 'phase-a';
|
|
952
|
-
} else if (
|
|
953
|
-
phaseNumber === '3' ||
|
|
954
|
-
phaseName.includes('追加QA') ||
|
|
955
|
-
phaseName.includes('QA') ||
|
|
956
|
-
phaseName.includes('試験') ||
|
|
957
|
-
phaseName.toLowerCase().includes('testing') ||
|
|
958
|
-
phaseName.toLowerCase().includes('additional qa')
|
|
959
|
-
) {
|
|
960
|
-
currentPhaseLabel = 'additional-qa';
|
|
961
|
-
} else if (
|
|
962
|
-
phaseNumber === 'B' ||
|
|
963
|
-
phaseNumber.toLowerCase() === 'b' ||
|
|
964
|
-
phaseName.includes('リリース準備テスト') ||
|
|
965
|
-
phaseName.toLowerCase().includes('release-test') ||
|
|
966
|
-
phaseName.toLowerCase().includes('release test')
|
|
967
|
-
) {
|
|
968
|
-
currentPhaseLabel = 'phase-b';
|
|
969
|
-
} else if (
|
|
970
|
-
phaseNumber === '4' ||
|
|
971
|
-
phaseName.includes('リリース準備') ||
|
|
972
|
-
phaseName.toLowerCase().includes('release-prep') ||
|
|
973
|
-
phaseName.toLowerCase().includes('release preparation')
|
|
974
|
-
) {
|
|
975
|
-
currentPhaseLabel = 'release-prep';
|
|
976
|
-
} else if (
|
|
977
|
-
phaseNumber === '5' ||
|
|
978
|
-
(phaseName.includes('リリース') && !phaseName.includes('準備')) ||
|
|
979
|
-
(phaseName.toLowerCase().includes('release') &&
|
|
980
|
-
!phaseName.toLowerCase().includes('prep'))
|
|
981
|
-
) {
|
|
982
|
-
currentPhaseLabel = 'release';
|
|
983
|
-
}
|
|
984
|
-
|
|
985
1050
|
console.log(
|
|
986
1051
|
`📌 Phase detected: ${phaseTitle} (number: ${phaseNumber}, label: ${currentPhaseLabel})`,
|
|
987
1052
|
);
|
|
@@ -999,7 +1064,7 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
999
1064
|
if (existingStorySummaries.has(storySummary)) {
|
|
1000
1065
|
console.log(`Skipping Story (already exists): ${storyTitle}`);
|
|
1001
1066
|
const existing = existingStories.find(
|
|
1002
|
-
(s:
|
|
1067
|
+
(s: JiraIssue) => s?.fields?.summary === storySummary,
|
|
1003
1068
|
);
|
|
1004
1069
|
if (existing) {
|
|
1005
1070
|
createdStories.push(existing.key);
|
|
@@ -1049,7 +1114,7 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
1049
1114
|
}
|
|
1050
1115
|
|
|
1051
1116
|
// JIRAペイロードを作成(issue type IDは既に取得済み)
|
|
1052
|
-
const storyPayload:
|
|
1117
|
+
const storyPayload: JIRAIssuePayload = {
|
|
1053
1118
|
fields: {
|
|
1054
1119
|
project: { key: projectMeta.jiraProjectKey },
|
|
1055
1120
|
summary: storySummary,
|
|
@@ -1109,23 +1174,24 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
1109
1174
|
|
|
1110
1175
|
// Epic Linkは手動設定が必要(JIRA Cloudの制約)
|
|
1111
1176
|
console.log(` ℹ️ Epic: ${epic.key} に手動でリンクしてください`);
|
|
1112
|
-
} catch (error:
|
|
1177
|
+
} catch (error: unknown) {
|
|
1113
1178
|
console.error(
|
|
1114
1179
|
` ❌ Failed to create Story "${storyTitle}":`,
|
|
1115
1180
|
error instanceof Error ? error.message : error,
|
|
1116
1181
|
);
|
|
1117
1182
|
|
|
1118
1183
|
// JIRA APIエラーの詳細を表示
|
|
1119
|
-
|
|
1184
|
+
const errorObj = error as { response?: { data?: { errors?: Record<string, unknown> } } };
|
|
1185
|
+
if (errorObj.response?.data) {
|
|
1120
1186
|
console.error(
|
|
1121
1187
|
' 📋 JIRA API Error Details:',
|
|
1122
|
-
JSON.stringify(
|
|
1188
|
+
JSON.stringify(errorObj.response.data, null, 2),
|
|
1123
1189
|
);
|
|
1124
1190
|
|
|
1125
1191
|
// Story Pointsフィールドのエラーの場合、警告を表示
|
|
1126
1192
|
if (
|
|
1127
|
-
|
|
1128
|
-
Object.keys(
|
|
1193
|
+
errorObj.response.data.errors &&
|
|
1194
|
+
Object.keys(errorObj.response.data.errors).some((key) =>
|
|
1129
1195
|
key.includes('customfield'),
|
|
1130
1196
|
)
|
|
1131
1197
|
) {
|
package/scripts/list-projects.ts
CHANGED
|
@@ -57,7 +57,7 @@ async function listProjects(): Promise<void> {
|
|
|
57
57
|
const { data } = await octokit.repos.getContent({
|
|
58
58
|
owner: org,
|
|
59
59
|
repo: repo.name,
|
|
60
|
-
path: `projects/${(projectEntry as
|
|
60
|
+
path: `projects/${(projectEntry as { name: string }).name}/.kiro/project.json`
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
if ('content' in data) {
|
|
@@ -74,7 +74,7 @@ async function listProjects(): Promise<void> {
|
|
|
74
74
|
}
|
|
75
75
|
} catch (error) {
|
|
76
76
|
// プロジェクトディレクトリに.kiro/project.jsonがない場合はスキップ
|
|
77
|
-
console.warn(`⚠️ Skipping project ${(projectEntry as
|
|
77
|
+
console.warn(`⚠️ Skipping project ${(projectEntry as { name: string }).name} in ${repo.name}:`, error instanceof Error ? error.message : 'Unknown error');
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -210,7 +210,7 @@ async function aggregateEstimates(): Promise<void> {
|
|
|
210
210
|
const specs = await octokit.paginate('GET /repos/{owner}/{repo}/contents/{path}', {
|
|
211
211
|
owner: org,
|
|
212
212
|
repo: repo.name,
|
|
213
|
-
path: `projects/${(projectEntry as
|
|
213
|
+
path: `projects/${(projectEntry as { name: string }).name}/.kiro/specs`,
|
|
214
214
|
per_page: 100
|
|
215
215
|
});
|
|
216
216
|
|
|
@@ -222,8 +222,8 @@ async function aggregateEstimates(): Promise<void> {
|
|
|
222
222
|
'name' in spec) {
|
|
223
223
|
// design.md を取得
|
|
224
224
|
try {
|
|
225
|
-
const projectName = (projectEntry as
|
|
226
|
-
const specName = (spec as
|
|
225
|
+
const projectName = (projectEntry as { name: string }).name;
|
|
226
|
+
const specName = (spec as { name: string }).name;
|
|
227
227
|
const { data: designFile } = await octokit.repos.getContent({
|
|
228
228
|
owner: org,
|
|
229
229
|
repo: repo.name,
|