Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mavera.io/llms.txt

Use this file to discover all available pages before exploring further.

Sprint Completion → Release Content

Scenario

When a sprint closes, marketing needs release notes and a changelog, not a list of Jira keys. This job pulls the most recently completed sprint, fetches all resolved issues, categorizes by type, then chains two Mavera Generate calls for polished release notes and a changelog. Flow: Jira GET /board/{id}/sprint (completed) → GET /sprint/{id}/issue (done) → categorize → Mavera POST /api/v1/generations (release notes + changelog)

Code

import os, requests, time, base64

DOMAIN, EMAIL = os.environ["JIRA_DOMAIN"], os.environ["JIRA_EMAIL"]
TOKEN, MV = os.environ["JIRA_API_TOKEN"], os.environ["MAVERA_API_KEY"]
AB, MB = f"https://{DOMAIN}.atlassian.net/rest/agile/1.0", "https://app.mavera.io/api/v1"
cred = base64.b64encode(f"{EMAIL}:{TOKEN}".encode()).decode()
JH = {"Authorization": f"Basic {cred}", "Content-Type": "application/json"}
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
BOARD_ID = 42

r = requests.get(f"{AB}/board/{BOARD_ID}/sprint", headers=JH, params={"state": "closed", "maxResults": 5})
if r.status_code == 429:
    time.sleep(int(r.headers.get("Retry-After", 30)))
    r = requests.get(f"{AB}/board/{BOARD_ID}/sprint", headers=JH, params={"state": "closed", "maxResults": 5})
r.raise_for_status()
sprints = r.json().get("values", [])
if not sprints:
    raise SystemExit("No completed sprints found")
sprint = sprints[-1]
print(f"Sprint: {sprint['name']}")

issues, start = [], 0
while True:
    r = requests.get(f"{AB}/sprint/{sprint['id']}/issue", headers=JH,
        params={"startAt": start, "maxResults": 100, "fields": "summary,issuetype,priority,resolution"})
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", 30)))
        continue
    r.raise_for_status()
    data = r.json()
    issues.extend(data.get("issues", []))
    if start + data["maxResults"] >= data["total"]:
        break
    start += data["maxResults"]
    time.sleep(0.5)

resolved = [i for i in issues if i["fields"].get("resolution")]
cats = {"features": [], "bugs": [], "improvements": []}
for iss in resolved:
    name = iss["fields"]["issuetype"]["name"].lower()
    entry = f"- [{iss['key']}] {iss['fields']['summary']} ({iss['fields']['priority']['name']})"
    if "story" in name or "feature" in name: cats["features"].append(entry)
    elif "bug" in name: cats["bugs"].append(entry)
    else: cats["improvements"].append(entry)

cat_block = "\n".join(f"{k.upper()} ({len(v)}):\n" + "\n".join(v) for k, v in cats.items() if v)

time.sleep(0.3)
notes = requests.post(f"{MB}/generations", headers=MH, json={
    "prompt": f"Release notes for '{sprint['name']}'.\n\n{cat_block[:5000]}\n\n"
              "Group by New Features/Bug Fixes/Improvements. User benefit first. Include Jira keys.",
}).json()
release_notes = notes.get("output", notes.get("content", ""))

time.sleep(0.3)
changelog = requests.post(f"{MB}/generations", headers=MH, json={
    "prompt": f"CHANGELOG.md for '{sprint['name']}'.\n\n{release_notes[:3000]}\n\n"
              "Keep-a-Changelog format (Added/Changed/Fixed). One line per item.",
}).json()

print(f"\nRELEASE NOTES:\n{release_notes[:2000]}")
print(f"\nCHANGELOG:\n{changelog.get('output', changelog.get('content', ''))[:1500]}")

Example Output

Sprint: Sprint 24 — API Hardening (2026-03-03 → 2026-03-14)
Resolved: 19 / 23 total issues

RELEASE NOTES:
## New Features
- Bulk issue import supports CSV up to 10K rows (PROJ-412)
- Real-time webhook status dashboard (PROJ-398)
## Bug Fixes
- Fixed 502 errors on boards with 500+ issues (PROJ-401)
- Email notifications work for plus-sign addresses (PROJ-389)

CHANGELOG:
## [2026-03-14] Sprint 24
### Added
- Bulk CSV import up to 10K rows (PROJ-412)
### Fixed
- 502 errors on large boards (PROJ-401)

Error Handling

The Agile API returns 404 if the board ID is wrong or the token lacks access. Verify via GET /rest/agile/1.0/board.
Sprints can close with unresolved issues (moved to backlog). The code filters on resolution != null. If all issues were moved, skip generation.
Sprint and board endpoints use /rest/agile/1.0, not /rest/api/3. Mixing them causes 404s.