ADR-0011: Epic Release Creation Architecture¶
Status: Accepted Enforced by: tests/test_epic.py, tests/test_release.py Date: 2026-03-01
Context¶
HydraFlow needed a mechanism to automatically create GitHub Releases when an epic completes (all child issues resolved). Several integration points were candidates:
PostMergeHandler— runs after each PR merge. Knows which issue just closed but has no visibility into the parent epic's completion state.EpicCompletionChecker— already checks whether all sub-issues are resolved and closes the epic. Has full context: epic title, sub-issue list, and completion status.EpicManager._try_auto_close()— the top-level entry point that fires when any child completes (on_child_completed()). Delegates toEpicCompletionCheckerfor the actual close-and-release workflow.
The release feature must extract a version from the epic title, create a git tag, create a GitHub Release with a changelog, and persist release state for crash-recovery and dashboard reporting.
Decision¶
Hook release creation into the EpicCompletionChecker._try_close_epic() path,
invoked from EpicManager._try_auto_close(). Do not place release logic in
PostMergeHandler or any other per-PR handler.
The call chain is:
PostMergeHandler.handle_approved()
└─ EpicManager.on_child_completed(epic, child)
└─ EpicManager._try_auto_close(epic)
└─ EpicCompletionChecker.check_and_close_epics(child)
└─ _try_close_epic(epic, title, body, sub_issues)
├─ _create_release_for_epic()
│ ├─ extract_version_from_title(epic_title)
│ ├─ PRManager.create_tag(tag)
│ ├─ PRManager.create_release(tag, title, changelog)
│ └─ StateTracker.upsert_release(release)
├─ post close comment (with release URL if created)
└─ close_issue(epic)
Key implementation details:
-
Tag and release are separate operations.
PRManager.create_tag()creates and pushes a git tag;PRManager.create_release()creates the GitHub Release referencing that tag. This two-step approach allows partial-failure handling (tag created but release failed). -
Version extraction from epic title. The
extract_version_from_title()utility parses a semver-like version from the epic's title. If no version is found, release creation is silently skipped. Therelease_version_sourceconfig field exists for future alternative sources but currently onlyepic_titleis implemented. -
Release state persisted in
StateData.releases. TheReleasemodel is stored in adict[str, Release]keyed by epic number (as string). This enables crash-recovery (re-check release existence before retrying) and dashboard reporting. -
Gated by
config.release_on_epic_close. The feature is opt-in; when disabled, the epic closes normally without any tag/release side-effects. -
Dry-run support. Both
create_tag()andcreate_release()inPRManagerrespect the globaldry_runflag, logging intent without executing.
Consequences¶
Positive:
- Release creation fires exactly once, at the moment all sub-issues are confirmed
complete — no risk of premature releases from partial merges.
- PostMergeHandler remains focused on single-PR lifecycle; epic-level concerns
stay in the epic subsystem.
- State persistence enables idempotent retries and dashboard visibility.
- The two-step tag/release flow allows fine-grained error handling and logging.
Trade-offs:
- Release creation depends on version being parseable from the epic title; epics
without a version string produce no release (by design, but could surprise users).
- If EpicCompletionChecker fails, the fallback direct-close path in
_try_auto_close() does not attempt release creation — only the primary
path creates releases.
- Two separate gh calls (tag + release) instead of a single atomic operation
means a tag could exist without a corresponding release on transient failure.
Alternatives considered¶
-
Hook into
PostMergeHandlerdirectly. Rejected: would require each merge handler to track epic-level state and detect "last child" completion — duplicating logic already inEpicCompletionChecker. -
Use
gh release create --target mainfor atomic tag+release. Not adopted: keeping tag creation separate (git tag+git push) gives explicit control over the tag ref and clearer error attribution. Thegh release createcommand can still reference an existing tag. -
Dedicated
ReleaseManagerservice. Not adopted at this stage: the release logic is compact enough to live inEpicCompletionChecker. A separate service can be extracted if release workflows grow more complex (e.g., artifact uploads, multi-repo coordination).
Related¶
- Source memory: Issue #1682 — [Memory] Epic release creation architecture
src/epic.py—EpicManager._try_auto_close(),EpicCompletionChecker._create_release_for_epic()src/pr_manager.py—PRManager.create_tag(),PRManager.create_release()src/models.py—Releasemodel,StateData.releases- PR #1690 — feat: create GitHub Release with changelog when epic closes