Jira import
This commit is contained in:
parent
cc9c6052d6
commit
780bc64adb
|
|
@ -13,6 +13,9 @@ python jira_bulk_import.py \
|
||||||
--project TURN \
|
--project TURN \
|
||||||
--csv issues.csv \
|
--csv issues.csv \
|
||||||
--create-missing-components
|
--create-missing-components
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--default-component General
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -84,7 +87,6 @@ class JiraClient:
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_project_components(self, project_key: str) -> dict[str, dict[str, Any]]:
|
def get_project_components(self, project_key: str) -> dict[str, dict[str, Any]]:
|
||||||
# GET /rest/api/3/project/{projectKeyOrId}/components
|
|
||||||
resp = self.get(f"/rest/api/3/project/{project_key}/components")
|
resp = self.get(f"/rest/api/3/project/{project_key}/components")
|
||||||
items = resp.json()
|
items = resp.json()
|
||||||
return {item["name"].strip().lower(): item for item in items}
|
return {item["name"].strip().lower(): item for item in items}
|
||||||
|
|
@ -98,7 +100,6 @@ class JiraClient:
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
def get_issue_types_for_project(self, project_key: str) -> dict[str, dict[str, Any]]:
|
def get_issue_types_for_project(self, project_key: str) -> dict[str, dict[str, Any]]:
|
||||||
# GET /rest/api/3/issue/createmeta/{projectIdOrKey}/issuetypes
|
|
||||||
resp = self.get(f"/rest/api/3/issue/createmeta/{project_key}/issuetypes")
|
resp = self.get(f"/rest/api/3/issue/createmeta/{project_key}/issuetypes")
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|
||||||
|
|
@ -110,7 +111,6 @@ class JiraClient:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_create_fields_metadata(self, project_key: str, issue_type_id: str) -> dict[str, Any]:
|
def get_create_fields_metadata(self, project_key: str, issue_type_id: str) -> dict[str, Any]:
|
||||||
# GET /rest/api/3/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}
|
|
||||||
resp = self.get(f"/rest/api/3/issue/createmeta/{project_key}/issuetypes/{issue_type_id}")
|
resp = self.get(f"/rest/api/3/issue/createmeta/{project_key}/issuetypes/{issue_type_id}")
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
return data.get("fields", {})
|
return data.get("fields", {})
|
||||||
|
|
@ -133,6 +133,11 @@ def parse_args() -> argparse.Namespace:
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Create missing project components automatically",
|
help="Create missing project components automatically",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--default-component",
|
||||||
|
default="General",
|
||||||
|
help="Fallback Jira component to use when a CSV row has no component",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
@ -163,7 +168,6 @@ def read_csv_issues(csv_path: str) -> list[CsvIssue]:
|
||||||
if not row or not any(cell.strip() for cell in row):
|
if not row or not any(cell.strip() for cell in row):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Pad short rows
|
|
||||||
padded = row + [""] * max(0, 7 - len(row))
|
padded = row + [""] * max(0, 7 - len(row))
|
||||||
|
|
||||||
summary = padded[0].strip()
|
summary = padded[0].strip()
|
||||||
|
|
@ -178,8 +182,9 @@ def read_csv_issues(csv_path: str) -> list[CsvIssue]:
|
||||||
print(f"Skipping row {idx}: empty Summary", file=sys.stderr)
|
print(f"Skipping row {idx}: empty Summary", file=sys.stderr)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
components = []
|
components: list[str] = []
|
||||||
for comp in (component_1, component_2):
|
for comp in (component_1, component_2):
|
||||||
|
comp = comp.strip()
|
||||||
if comp and comp not in components:
|
if comp and comp not in components:
|
||||||
components.append(comp)
|
components.append(comp)
|
||||||
|
|
||||||
|
|
@ -233,12 +238,14 @@ def ensure_components(
|
||||||
project_key: str,
|
project_key: str,
|
||||||
issues: list[CsvIssue],
|
issues: list[CsvIssue],
|
||||||
create_missing: bool,
|
create_missing: bool,
|
||||||
|
default_component: str,
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
existing = jira.get_project_components(project_key)
|
existing = jira.get_project_components(project_key)
|
||||||
|
|
||||||
needed_names: list[str] = []
|
needed_names: list[str] = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
for comp in issue.components:
|
issue_components = issue.components or [default_component]
|
||||||
|
for comp in issue_components:
|
||||||
key = normalize(comp)
|
key = normalize(comp)
|
||||||
if key and key not in existing and comp not in needed_names:
|
if key and key not in existing and comp not in needed_names:
|
||||||
needed_names.append(comp)
|
needed_names.append(comp)
|
||||||
|
|
@ -265,6 +272,7 @@ def build_issue_payload(
|
||||||
issue_types_by_name: dict[str, dict[str, Any]],
|
issue_types_by_name: dict[str, dict[str, Any]],
|
||||||
create_fields_by_issue_type_id: dict[str, dict[str, Any]],
|
create_fields_by_issue_type_id: dict[str, dict[str, Any]],
|
||||||
components_by_name: dict[str, dict[str, Any]],
|
components_by_name: dict[str, dict[str, Any]],
|
||||||
|
default_component: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
work_type_key = normalize(issue.work_type)
|
work_type_key = normalize(issue.work_type)
|
||||||
if work_type_key not in issue_types_by_name:
|
if work_type_key not in issue_types_by_name:
|
||||||
|
|
@ -290,21 +298,18 @@ def build_issue_payload(
|
||||||
if issue.priority and "priority" in fields_meta:
|
if issue.priority and "priority" in fields_meta:
|
||||||
fields["priority"] = {"name": issue.priority}
|
fields["priority"] = {"name": issue.priority}
|
||||||
|
|
||||||
if issue.components and "components" in fields_meta:
|
effective_components = issue.components or [default_component]
|
||||||
comps = []
|
|
||||||
for comp_name in issue.components:
|
|
||||||
key = normalize(comp_name)
|
|
||||||
component = components_by_name.get(key)
|
|
||||||
if not component:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Component '{comp_name}' not found for issue '{issue.summary}'"
|
|
||||||
)
|
|
||||||
comps.append({"id": component["id"]})
|
|
||||||
fields["components"] = comps
|
|
||||||
|
|
||||||
# Intentionally not sending "status".
|
comps = []
|
||||||
# Jira creation should start in the workflow default state unless you add
|
for comp_name in effective_components:
|
||||||
# transition logic after creation.
|
key = normalize(comp_name)
|
||||||
|
component = components_by_name.get(key)
|
||||||
|
if not component:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Component '{comp_name}' not found for issue '{issue.summary}'"
|
||||||
|
)
|
||||||
|
comps.append({"id": component["id"]})
|
||||||
|
fields["components"] = comps
|
||||||
|
|
||||||
return {"fields": fields}
|
return {"fields": fields}
|
||||||
|
|
||||||
|
|
@ -350,6 +355,7 @@ def main() -> int:
|
||||||
project_key=args.project,
|
project_key=args.project,
|
||||||
issues=issues,
|
issues=issues,
|
||||||
create_missing=args.create_missing_components,
|
create_missing=args.create_missing_components,
|
||||||
|
default_component=args.default_component,
|
||||||
)
|
)
|
||||||
|
|
||||||
payloads: list[dict[str, Any]] = []
|
payloads: list[dict[str, Any]] = []
|
||||||
|
|
@ -361,6 +367,7 @@ def main() -> int:
|
||||||
issue_types_by_name=issue_types_by_name,
|
issue_types_by_name=issue_types_by_name,
|
||||||
create_fields_by_issue_type_id=create_fields_by_issue_type_id,
|
create_fields_by_issue_type_id=create_fields_by_issue_type_id,
|
||||||
components_by_name=components_by_name,
|
components_by_name=components_by_name,
|
||||||
|
default_component=args.default_component,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -396,7 +403,6 @@ def main() -> int:
|
||||||
created_total += len(created)
|
created_total += len(created)
|
||||||
error_total += len(errors)
|
error_total += len(errors)
|
||||||
|
|
||||||
# Gentle pause between batches to avoid hammering the API.
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
@ -414,11 +420,3 @@ if __name__ == "__main__":
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"\nFATAL: {exc}", file=sys.stderr)
|
print(f"\nFATAL: {exc}", file=sys.stderr)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
# python jira_bulk_import.py \
|
|
||||||
# --site https://YOURDOMAIN.atlassian.net \
|
|
||||||
# --email you@example.com \
|
|
||||||
# --api-token YOUR_API_TOKEN \
|
|
||||||
# --project TUR \
|
|
||||||
# --csv TODO.csv \
|
|
||||||
# --create-missing-components
|
|
||||||
Loading…
Reference in New Issue