atlasdocs-jira 0.9.0__tar.gz

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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: atlasdocs-jira
3
+ Version: 0.9.0
4
+ Summary: Jira integration scripts for atlasdocs-theme
5
+ Author-email: Jason Oliver <jason.oliver@cern.ch>
6
+ License-Expression: Apache-2.0
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: python-dotenv>=1.0
9
+ Requires-Dist: requests>=2.28
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def __getattr__(name: str):
5
+ if name == "create_jira_pages":
6
+ from .prepare_jira_pages import create_jira_pages
7
+ return create_jira_pages
8
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
9
+
10
+
11
+ __all__ = ["create_jira_pages"]
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Fetch Jira project issues and save them as JSON files.
5
+
6
+ Reads which projects to fetch from jira.toml in the hub directory.
7
+ Writes one file per project:
8
+ data/jira/{KEY}.json
9
+
10
+ Credentials: set JIRA_TOKEN (personal access token) or
11
+ JIRA_USER + JIRA_PASSWORD in the environment.
12
+
13
+ Usage examples
14
+ --------------
15
+ Fetch all projects from jira.toml:
16
+ python -m atlasdocs_jira.fetch_jira
17
+
18
+ Specific projects:
19
+ python -m atlasdocs_jira.fetch_jira --projects ATLASDOC
20
+ python -m atlasdocs_jira.fetch_jira --projects ATLASDOC,ATWIKIPHYS
21
+
22
+ Custom JQL query (saves to data/jira/tickets.json):
23
+ python -m atlasdocs_jira.fetch_jira --jql "project = FOO AND status = Open"
24
+
25
+ Only issues created in the last 3 years:
26
+ python -m atlasdocs_jira.fetch_jira --since -3y
27
+
28
+ List accessible projects:
29
+ python -m atlasdocs_jira.fetch_jira --list-projects
30
+ python -m atlasdocs_jira.fetch_jira --list-projects atlas
31
+
32
+ Re-fetch even if cached:
33
+ python -m atlasdocs_jira.fetch_jira --refresh
34
+
35
+ Dry-run (print first 10 tickets, don't save):
36
+ python -m atlasdocs_jira.fetch_jira --dry-run
37
+
38
+ Pixi (args after --):
39
+ pixi run fetch-jira
40
+ pixi run fetch-jira -- --projects ATLASDOC
41
+ """
42
+
43
+ import argparse
44
+ import base64
45
+ import json
46
+ import os
47
+ import re
48
+ import sys
49
+ import time
50
+ import tomllib
51
+ import urllib.parse
52
+ import urllib.request
53
+ from datetime import date, timedelta
54
+ from pathlib import Path
55
+
56
+ from dotenv import load_dotenv
57
+ load_dotenv(Path('.env').resolve())
58
+
59
+ # ── Configuration ──────────────────────────────────────────────────────────────
60
+
61
+ JIRA_SITE = "https://its.cern.ch/jira"
62
+ DATA_DIR = "data/jira"
63
+ JIRA_CONFIG_FILE = "jira.toml"
64
+ PAGE_SIZE = 1000
65
+ REQUEST_DELAY = 0.3
66
+
67
+ FIELDS = [
68
+ "summary", "description", "status", "priority",
69
+ "assignee", "reporter", "created", "updated",
70
+ "labels", "components", "issuetype", "resolution",
71
+ "resolutiondate", "comment",
72
+ ]
73
+
74
+
75
+ # ── Config loading ─────────────────────────────────────────────────────────────
76
+
77
+ def load_jira_config(hub_dir: str = ".") -> dict:
78
+ """Load jira.toml. Returns {projects: [{key, label}], site}."""
79
+ config_path = Path(hub_dir) / JIRA_CONFIG_FILE
80
+ if not config_path.exists():
81
+ return {"projects": [], "site": JIRA_SITE}
82
+ with open(config_path, "rb") as f:
83
+ data = tomllib.load(f)
84
+ return {
85
+ "projects": data.get("projects", []),
86
+ "site": data.get("site", JIRA_SITE),
87
+ }
88
+
89
+
90
+ # ── Credentials ────────────────────────────────────────────────────────────────
91
+
92
+ def load_credentials() -> tuple[str, str, bool]:
93
+ """Return (user, secret, is_token). Exits on missing credentials."""
94
+ user = os.environ.get("JIRA_USER", "")
95
+ token = os.environ.get("JIRA_TOKEN")
96
+ password = os.environ.get("JIRA_PASSWORD")
97
+ if token:
98
+ return user, token, True
99
+ if password:
100
+ if not user:
101
+ sys.exit("Error: JIRA_USER must be set when using JIRA_PASSWORD.")
102
+ return user, password, False
103
+ sys.exit("Error: JIRA_TOKEN or JIRA_PASSWORD not set in environment.")
104
+
105
+
106
+ def _auth_header(user: str, secret: str, is_token: bool) -> str:
107
+ if is_token:
108
+ return f"Bearer {secret}"
109
+ return "Basic " + base64.b64encode(f"{user}:{secret}".encode()).decode()
110
+
111
+
112
+ # ── HTTP ───────────────────────────────────────────────────────────────────────
113
+
114
+ def _get(auth: str, url: str) -> dict:
115
+ req = urllib.request.Request(url, headers={
116
+ "Authorization": auth,
117
+ "Accept": "application/json",
118
+ })
119
+ with urllib.request.urlopen(req, timeout=60) as resp:
120
+ return json.loads(resp.read())
121
+
122
+
123
+ # ── Date helpers ───────────────────────────────────────────────────────────────
124
+
125
+ def _since_to_jql_date(since: str) -> str:
126
+ """Convert -3y / -6m / -2w / -30d or ISO date to a quoted JQL date string."""
127
+ m = re.fullmatch(r"-?(\d+)([ymwd])", since)
128
+ if not m:
129
+ return f'"{since}"'
130
+ n, unit = int(m.group(1)), m.group(2)
131
+ today = date.today()
132
+ if unit == "y":
133
+ cutoff = today.replace(year=today.year - n)
134
+ elif unit == "m":
135
+ month = today.month - n
136
+ year = today.year + (month - 1) // 12
137
+ month = (month - 1) % 12 + 1
138
+ cutoff = today.replace(year=year, month=month)
139
+ elif unit == "w":
140
+ cutoff = today - timedelta(weeks=n)
141
+ else:
142
+ cutoff = today - timedelta(days=n)
143
+ return f'"{cutoff.isoformat()}"'
144
+
145
+
146
+ # ── Fetch ──────────────────────────────────────────────────────────────────────
147
+
148
+ def fetch_projects(auth: str, site: str) -> list[dict]:
149
+ return sorted(_get(auth, f"{site}/rest/api/2/project"), key=lambda p: p["key"])
150
+
151
+
152
+ def fetch_page(auth: str, site: str, jql: str, start_at: int) -> dict:
153
+ params = urllib.parse.urlencode({
154
+ "jql": jql,
155
+ "startAt": start_at,
156
+ "maxResults": PAGE_SIZE,
157
+ "fields": ",".join(FIELDS),
158
+ })
159
+ return _get(auth, f"{site}/rest/api/2/search?{params}")
160
+
161
+
162
+ def fetch_all(auth: str, site: str, jql: str) -> list[dict]:
163
+ tickets: list[dict] = []
164
+ start_at = 0
165
+ while True:
166
+ print(f"[jira] {start_at}–{start_at + PAGE_SIZE - 1}…", end="", flush=True)
167
+ data = fetch_page(auth, site, jql, start_at)
168
+ issues = data.get("issues", [])
169
+ total = data.get("total", 0)
170
+ tickets.extend(issues)
171
+ print(f" +{len(issues)} ({len(tickets)}/{total})")
172
+ if not issues or len(tickets) >= total:
173
+ break
174
+ start_at += len(issues)
175
+ time.sleep(REQUEST_DELAY)
176
+ return tickets
177
+
178
+
179
+ def _print_row(t: dict) -> None:
180
+ f = t.get("fields", {})
181
+ status = (f.get("status") or {}).get("name", "?")
182
+ priority = (f.get("priority") or {}).get("name", "?")
183
+ summary = (f.get("summary") or "").strip()
184
+ updated = (f.get("updated") or "")[:10]
185
+ print(f" {t['key']:<18} [{status:<12}] [{priority:<16}] {updated} {summary[:70]}")
186
+
187
+
188
+ # ── Save ───────────────────────────────────────────────────────────────────────
189
+
190
+ def save_project(key: str, tickets: list[dict], data_dir: str) -> Path:
191
+ out_dir = Path(data_dir)
192
+ out_dir.mkdir(parents=True, exist_ok=True)
193
+ out_path = out_dir / f"{key}.json"
194
+ out_path.write_text(json.dumps(tickets, indent=2, ensure_ascii=False))
195
+ return out_path
196
+
197
+
198
+ # ── Main ───────────────────────────────────────────────────────────────────────
199
+
200
+ def main() -> None:
201
+ parser = argparse.ArgumentParser(
202
+ description="Fetch Jira project issues and save as JSON.",
203
+ formatter_class=argparse.RawDescriptionHelpFormatter,
204
+ epilog=__doc__.split("Usage examples")[1] if __doc__ and "Usage examples" in __doc__ else "",
205
+ )
206
+ parser.add_argument("--projects", metavar="KEY1,KEY2",
207
+ help="Comma-separated project keys (default: all projects in jira.toml)")
208
+ parser.add_argument("--jql", help="Custom JQL query (saves to data/jira/tickets.json)")
209
+ parser.add_argument("--since", metavar="DATE_OR_OFFSET",
210
+ help="Only fetch issues created on/after this date (e.g. -3y, 2022-01-01)")
211
+ parser.add_argument("--list-projects", nargs="?", const="", metavar="PATTERN",
212
+ help="List accessible projects, optionally filtered by name/key pattern")
213
+ parser.add_argument("--refresh", action="store_true",
214
+ help="Re-fetch even if cache exists")
215
+ parser.add_argument("--dry-run", action="store_true",
216
+ help="Fetch 10 tickets, print summaries, exit without saving")
217
+ parser.add_argument("--site", default=None,
218
+ help=f"Jira base URL (default: from jira.toml or {JIRA_SITE})")
219
+ parser.add_argument("--data-dir", default=DATA_DIR,
220
+ help=f"Output directory (default: {DATA_DIR})")
221
+ args = parser.parse_args()
222
+
223
+ hub_dir = str(Path.cwd())
224
+ config = load_jira_config(hub_dir)
225
+ site = args.site or config["site"]
226
+
227
+ user, secret, is_token = load_credentials()
228
+ auth = _auth_header(user, secret, is_token)
229
+
230
+ resolved_data_dir = str(Path(hub_dir) / args.data_dir)
231
+
232
+ # ── list-projects ──────────────────────────────────────────────────────────
233
+ if args.list_projects is not None:
234
+ pattern = args.list_projects.lower()
235
+ projects = fetch_projects(auth, site)
236
+ if pattern:
237
+ projects = [p for p in projects
238
+ if pattern in p["key"].lower() or pattern in p["name"].lower()]
239
+ print(f"{'KEY':<24} {'TYPE':<14} {'ID':<8} NAME")
240
+ print("-" * 80)
241
+ for p in projects:
242
+ print(f" {p['key']:<22} {p.get('projectTypeKey',''):<14} {p['id']:<8} {p['name']}")
243
+ print(f"\n{len(projects)} project(s) shown")
244
+ return
245
+
246
+ # ── custom JQL mode ────────────────────────────────────────────────────────
247
+ if args.jql:
248
+ out_file = Path(resolved_data_dir) / "tickets.json"
249
+ if out_file.exists() and not args.refresh:
250
+ existing = json.loads(out_file.read_text())
251
+ print(f"Cache hit: {len(existing):,} tickets in {out_file} (use --refresh to re-fetch)")
252
+ return
253
+ if args.dry_run:
254
+ data = fetch_page(auth, site, args.jql, 0)
255
+ tickets = data.get("issues", [])[:10]
256
+ print(f"DRY RUN JQL: {args.jql}\nTotal: {data.get('total',0):,} (showing {len(tickets)})\n")
257
+ for t in tickets:
258
+ _print_row(t)
259
+ return
260
+ tickets = fetch_all(auth, site, args.jql)
261
+ out_file.parent.mkdir(parents=True, exist_ok=True)
262
+ out_file.write_text(json.dumps(tickets, indent=2, ensure_ascii=False))
263
+ print(f"\nSaved {len(tickets):,} tickets → {out_file}")
264
+ return
265
+
266
+ # ── per-project mode ───────────────────────────────────────────────────────
267
+ if args.projects:
268
+ keys = [k.strip().upper() for k in args.projects.split(",") if k.strip()]
269
+ else:
270
+ keys = [p["key"] for p in config["projects"]]
271
+ if not keys:
272
+ parser.error("No projects specified — use --projects or add entries to jira.toml")
273
+
274
+ since_clause = f" AND created >= {_since_to_jql_date(args.since)}" if args.since else ""
275
+
276
+ if args.dry_run:
277
+ jql = f"project = {keys[0]}{since_clause} ORDER BY updated DESC"
278
+ data = fetch_page(auth, site, jql, 0)
279
+ tickets = data.get("issues", [])[:10]
280
+ print(f"DRY RUN — {keys[0]} JQL: {jql}\nTotal: {data.get('total',0):,} (showing {len(tickets)})\n")
281
+ for t in tickets:
282
+ _print_row(t)
283
+ return
284
+
285
+ print(f"[jira] Fetching {len(keys)} project(s) → {resolved_data_dir}")
286
+ grand_total = skipped = errors = 0
287
+ for i, key in enumerate(keys, 1):
288
+ out_file = Path(resolved_data_dir) / f"{key}.json"
289
+ if out_file.exists() and not args.refresh:
290
+ existing = json.loads(out_file.read_text())
291
+ skipped += 1
292
+ grand_total += len(existing)
293
+ print(f"[jira] [{i}/{len(keys)}] {key}: {len(existing):,} tickets (cached)")
294
+ continue
295
+ jql = f"project = {key}{since_clause} ORDER BY updated DESC"
296
+ try:
297
+ tickets = fetch_all(auth, site, jql)
298
+ except Exception as e:
299
+ print(f"[jira] [{i}/{len(keys)}] {key}: ERROR — {e}")
300
+ errors += 1
301
+ continue
302
+ if tickets:
303
+ save_project(key, tickets, resolved_data_dir)
304
+ grand_total += len(tickets)
305
+ print(f"[jira] [{i}/{len(keys)}] {key}: {len(tickets):,} tickets → {out_file}")
306
+ else:
307
+ print(f"[jira] [{i}/{len(keys)}] {key}: no tickets")
308
+
309
+ fetched = len(keys) - skipped - errors
310
+ print(f"[jira] Total: {grand_total:,} tickets ({fetched} fetched, {skipped} cached{', ' + str(errors) + ' error(s)' if errors else ''})")
311
+
312
+ if errors:
313
+ sys.exit(1)
314
+
315
+
316
+ if __name__ == "__main__":
317
+ main()
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Generate MkDocs pages for Jira projects from fetched JSON files.
5
+
6
+ Reads which projects to include from jira.toml in the hub directory.
7
+ For each project that has a data/jira/{KEY}.json file:
8
+ - Copies the JSON to docs/jira/projects/{KEY}/{KEY}.json
9
+ - Writes docs/jira/projects/{KEY}/index.md (JS loader page)
10
+ Writes docs/jira/index.md as a hub page linking to all projects.
11
+ Writes config/zensical/nav-jira.toml with the Jira nav section.
12
+
13
+ Usage examples
14
+ --------------
15
+ python -m atlasdocs_jira.prepare_jira_pages
16
+
17
+ Pixi:
18
+ pixi run prepare-jira-pages
19
+ """
20
+
21
+ import json
22
+ import os
23
+ import shutil
24
+ import sys
25
+ import tomllib
26
+ from datetime import datetime
27
+ from pathlib import Path
28
+
29
+
30
+ # ── Configuration ──────────────────────────────────────────────────────────────
31
+
32
+ JIRA_CONFIG_FILE = "jira.toml"
33
+ DATA_DIR = "data/jira"
34
+ DOCS_OUTPUT_DIR = "docs/jira"
35
+ NAV_OUTPUT_FILE = "config/zensical/nav-jira.toml"
36
+
37
+ JIRA_SITE = "https://its.cern.ch/jira"
38
+
39
+ PAGE_ICON = "lucide/ticket"
40
+ PAGE_TAGS_BASE = ['"★ JIRA"']
41
+
42
+
43
+ # ── Config loading ─────────────────────────────────────────────────────────────
44
+
45
+ def load_jira_config(hub_dir: str = ".") -> list[dict]:
46
+ """Load jira.toml and return [{key, label}, ...]."""
47
+ config_path = Path(hub_dir) / JIRA_CONFIG_FILE
48
+ if not config_path.exists():
49
+ return []
50
+ with open(config_path, "rb") as f:
51
+ data = tomllib.load(f)
52
+ site = data.get("site", JIRA_SITE)
53
+ return [
54
+ {
55
+ "key": p["key"].upper(),
56
+ "label": p.get("label", p["key"].upper()),
57
+ "site": site,
58
+ }
59
+ for p in data.get("projects", [])
60
+ ]
61
+
62
+
63
+ # ── Stats helpers ──────────────────────────────────────────────────────────────
64
+
65
+ def _project_stats(json_path: str) -> dict:
66
+ """Return {total, open, closed, first_year, year_counts} from a project JSON."""
67
+ if not os.path.exists(json_path):
68
+ return {}
69
+ with open(json_path) as f:
70
+ try:
71
+ issues = json.load(f)
72
+ except Exception:
73
+ return {}
74
+
75
+ total = len(issues)
76
+ open_ = sum(1 for i in issues if i.get("fields", {}).get("resolution") is None)
77
+ closed = total - open_
78
+
79
+ years = [int(i["fields"]["created"][:4])
80
+ for i in issues if i.get("fields", {}).get("created")]
81
+ first_year = min(years) if years else None
82
+
83
+ year_map: dict[int, int] = {}
84
+ for y in years:
85
+ year_map[y] = year_map.get(y, 0) + 1
86
+ year_counts = sorted(year_map.items())
87
+
88
+ return {
89
+ "total": total,
90
+ "open": open_,
91
+ "closed": closed,
92
+ "first_year": first_year,
93
+ "year_counts": year_counts,
94
+ }
95
+
96
+
97
+ # ── Frontmatter ────────────────────────────────────────────────────────────────
98
+
99
+ def _frontmatter(title: str, tags: list[str] | None = None) -> str:
100
+ lines = [
101
+ "---",
102
+ f"title: {title}",
103
+ f"icon: {PAGE_ICON}",
104
+ "hide:",
105
+ " - toc",
106
+ "pagestatus: script",
107
+ f"dategenerated: \"{datetime.now().strftime('%Y-%m-%d')}\"",
108
+ ]
109
+ if tags:
110
+ lines.append("tags:")
111
+ for tag in tags:
112
+ lines.append(f" - {tag}")
113
+ lines += ["---", ""]
114
+ return "\n".join(lines)
115
+
116
+
117
+ # ── Per-project page ───────────────────────────────────────────────────────────
118
+
119
+ def _generate_project_page(
120
+ output_dir: str,
121
+ key: str,
122
+ label: str,
123
+ data_dir: str,
124
+ site: str,
125
+ ) -> bool:
126
+ """Generate docs/jira/projects/{KEY}/index.md and copy JSON. Returns True on success."""
127
+ src_json = os.path.join(data_dir, f"{key}.json")
128
+ if not os.path.exists(src_json):
129
+ print(f"[SKIP] {key}: data/jira/{key}.json not found (run fetch-jira first)")
130
+ return False
131
+
132
+ os.makedirs(output_dir, exist_ok=True)
133
+
134
+ dst_json = os.path.join(output_dir, f"{key}.json")
135
+ shutil.copy2(src_json, dst_json)
136
+
137
+ stats = _project_stats(src_json)
138
+ total = stats.get("total", 0)
139
+
140
+ fm = _frontmatter(
141
+ title=f'"{label}"',
142
+ tags=list(PAGE_TAGS_BASE) + [f'"▶ {key}"'],
143
+ )
144
+ md = [
145
+ fm,
146
+ f'<div id="jira-project"',
147
+ f' data-json="./{key}.json"',
148
+ f' data-project="{key}"',
149
+ f' data-jira-base="{site}"></div>',
150
+ '<script src="/assets/javascripts/jira_project_loader.js"></script>',
151
+ "",
152
+ ]
153
+ with open(os.path.join(output_dir, "index.md"), "w") as f:
154
+ f.write("\n".join(md))
155
+
156
+ print(f"[DONE] docs/jira/projects/{key}/index.md — {total} issues")
157
+ return True
158
+
159
+
160
+ # ── Hub page ───────────────────────────────────────────────────────────────────
161
+
162
+ def _generate_hub_page(projects: list[dict], output_path: str, data_dir: str) -> None:
163
+ """Write docs/jira/index.md with grid cards for each project."""
164
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
165
+
166
+ fm = _frontmatter(title='"Jira Projects"')
167
+ lines = [fm, '<div class="grid cards" markdown>', ""]
168
+
169
+ for p in projects:
170
+ key = p["key"]
171
+ label = p["label"]
172
+ site = p["site"]
173
+ stats = _project_stats(os.path.join(data_dir, f"{key}.json"))
174
+ total = stats.get("total", "")
175
+ open_ = stats.get("open", "")
176
+ desc = f"{open_} open · {total} total" if total else ""
177
+
178
+ lines += [
179
+ f"- **[{label}](projects/{key}/)**",
180
+ "",
181
+ f" ---",
182
+ "",
183
+ ]
184
+ if desc:
185
+ lines += [f" {desc}", ""]
186
+ lines += [
187
+ f" [:lucide-external-link: {key}]({site}/projects/{key}) "
188
+ f"[:lucide-list: Browse](projects/{key}/)",
189
+ "",
190
+ ]
191
+
192
+ lines += ["</div>", ""]
193
+
194
+ with open(output_path, "w") as f:
195
+ f.write("\n".join(lines))
196
+
197
+ print(f"[DONE] {output_path} — {len(projects)} projects")
198
+
199
+
200
+ # ── Nav ────────────────────────────────────────────────────────────────────────
201
+
202
+ def _generate_nav(projects: list[dict], output_path: str) -> None:
203
+ """Write config/zensical/nav-jira.toml."""
204
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
205
+
206
+ lines = ["nav = [", ' { "Jira" = [']
207
+ lines.append(' { "Overview" = "jira/index.md" },')
208
+ for p in projects:
209
+ key = p["key"]
210
+ label = p["label"]
211
+ lines.append(f' {{ "{label}" = "jira/projects/{key}/index.md" }},')
212
+ lines += [" ]},", "]", ""]
213
+
214
+ with open(output_path, "w") as f:
215
+ f.write("\n".join(lines))
216
+
217
+ print(f"[DONE] {output_path} — {len(projects)} projects")
218
+
219
+
220
+ # ── Prepare ────────────────────────────────────────────────────────────────────
221
+
222
+ def create_jira_pages(
223
+ hub_dir: str = ".",
224
+ data_dir: str | None = None,
225
+ docs_output_dir: str | None = None,
226
+ nav_output_file: str | None = None,
227
+ ) -> None:
228
+ """Generate MkDocs pages for Jira projects fetched into data/jira/."""
229
+ hub = Path(hub_dir)
230
+ resolved_data_dir = str(hub / (data_dir or DATA_DIR))
231
+ resolved_docs_dir = str(hub / (docs_output_dir or DOCS_OUTPUT_DIR))
232
+ resolved_nav_file = str(hub / (nav_output_file or NAV_OUTPUT_FILE))
233
+
234
+ config_projects = load_jira_config(hub_dir)
235
+ if not config_projects:
236
+ print("[INFO] No projects found in jira.toml — nothing to generate.")
237
+ return
238
+
239
+ generated: list[dict] = []
240
+ for p in config_projects:
241
+ output_dir = os.path.join(resolved_docs_dir, "projects", p["key"])
242
+ ok = _generate_project_page(
243
+ output_dir=output_dir,
244
+ key=p["key"],
245
+ label=p["label"],
246
+ data_dir=resolved_data_dir,
247
+ site=p["site"],
248
+ )
249
+ if ok:
250
+ generated.append(p)
251
+
252
+ _generate_hub_page(generated, os.path.join(resolved_docs_dir, "index.md"), resolved_data_dir)
253
+ _generate_nav(generated, resolved_nav_file)
254
+
255
+ print(f"[FINISH] Generated {len(generated)} Jira project page(s).")
256
+
257
+
258
+ def main() -> None:
259
+ hub_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
260
+ create_jira_pages(hub_dir=str(hub_dir))
261
+
262
+
263
+ if __name__ == "__main__":
264
+ main()
@@ -0,0 +1,19 @@
1
+ [workspace]
2
+ name = "atlasdocs-jira-dev"
3
+ description = "Development environment for the atlasdocs-jira package"
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "linux-64"]
6
+
7
+ [dependencies]
8
+ python = ">=3.11,<3.15"
9
+ uv = ">=0.4"
10
+ requests = ">=2.0"
11
+
12
+ [tasks.build]
13
+ cmd = "uv build"
14
+ description = "Build wheel and sdist"
15
+
16
+ [tasks.newrelease]
17
+ cmd = "bash scripts/release.sh"
18
+ depends-on = []
19
+ description = "Bump version in pyproject.toml, build, and publish to PyPI"
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "atlasdocs-jira"
7
+ version = "0.9.0"
8
+ description = "Jira integration scripts for atlasdocs-theme"
9
+ authors = [
10
+ { name = "Jason Oliver", email = "jason.oliver@cern.ch" }
11
+ ]
12
+ license = "Apache-2.0"
13
+ requires-python = ">=3.9"
14
+ dependencies = ["requests>=2.28", "python-dotenv>=1.0"]
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ include = ["atlasdocs_jira"]
18
+
19
+ [project.scripts]
20
+ fetch-jira = "atlasdocs_jira.fetch_jira:main"
21
+ prepare-jira-pages = "atlasdocs_jira.prepare_jira_pages:main"
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PYPROJECT="$(dirname "$0")/../pyproject.toml"
5
+ PYPROJECT="$(realpath "$PYPROJECT")"
6
+ PKG_DIR="$(dirname "$PYPROJECT")"
7
+
8
+ current_version() {
9
+ grep '^version' "$PYPROJECT" | sed 's/version = "\(.*\)"/\1/'
10
+ }
11
+
12
+ bump_version() {
13
+ local part="${1:-patch}"
14
+ local ver
15
+ ver="$(current_version)"
16
+ IFS='.' read -r major minor patch <<< "$ver"
17
+ case "$part" in
18
+ major) major=$((major + 1)); minor=0; patch=0 ;;
19
+ minor) minor=$((minor + 1)); patch=0 ;;
20
+ patch) patch=$((patch + 1)) ;;
21
+ *) echo "Unknown part: $part (use major|minor|patch)"; exit 1 ;;
22
+ esac
23
+ echo "${major}.${minor}.${patch}"
24
+ }
25
+
26
+ set_version() {
27
+ local new="$1"
28
+ sed -i '' "s/^version = \".*\"/version = \"${new}\"/" "$PYPROJECT"
29
+ echo "Version set to ${new}"
30
+ }
31
+
32
+ PART="${1:-}"
33
+ SKIP_PUBLISH="${2:-}"
34
+ CURRENT="$(current_version)"
35
+
36
+ if [[ -z "$PART" ]]; then
37
+ echo "Current version: ${CURRENT}"
38
+ echo "Increment: [1] patch [2] minor [3] major"
39
+ read -r -p "Choice [1]: " choice
40
+ case "${choice:-1}" in
41
+ 1|patch) PART="patch" ;;
42
+ 2|minor) PART="minor" ;;
43
+ 3|major) PART="major" ;;
44
+ *) echo "Invalid choice."; exit 1 ;;
45
+ esac
46
+ fi
47
+
48
+ if [[ "$PART" == exact:* ]]; then
49
+ NEW_VERSION="${PART#exact:}"
50
+ else
51
+ NEW_VERSION="$(bump_version "$PART")"
52
+ fi
53
+
54
+ echo "Current: ${CURRENT} → New: ${NEW_VERSION} (${PART})"
55
+ read -r -p "Continue? [y/N] " confirm
56
+ [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
57
+
58
+ set_version "$NEW_VERSION"
59
+
60
+ echo ""
61
+ echo "▶ uv build"
62
+ uv build --project "$PKG_DIR"
63
+
64
+ if [[ "$SKIP_PUBLISH" != "--no-publish" ]]; then
65
+ echo ""
66
+ echo "▶ uv publish"
67
+ uv publish dist/atlasdocs_jira-"${NEW_VERSION}"*.whl dist/atlasdocs_jira-"${NEW_VERSION}".tar.gz
68
+ fi
69
+
70
+ echo ""
71
+ echo "▶ git add + commit + tag"
72
+ REPO_ROOT="$(git -C "$PKG_DIR" rev-parse --show-toplevel)"
73
+ git -C "$REPO_ROOT" add "$PYPROJECT"
74
+ git -C "$REPO_ROOT" commit -m "chore(jira): release ${NEW_VERSION}"
75
+ git -C "$REPO_ROOT" tag "atlasdocs-jira/v${NEW_VERSION}"
76
+
77
+ echo ""
78
+ echo "✓ Released atlasdocs-jira ${NEW_VERSION}"
79
+ echo " Push with: git push && git push --tags"