From e3b4b2cdfda6f2a5b4562f08fd7108a5831e1ad5 Mon Sep 17 00:00:00 2001 From: Andriujose <43181885+andriujoseba@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:48:55 +0000 Subject: [PATCH] feat: announce release initialization --- .../issueflow-reconcile.sh | 18 ++++++ changelog.d/253.md | 3 + test/issueflow-reconcile.test.sh | 63 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 changelog.d/253.md diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index ab331bd..b936f97 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -746,6 +746,24 @@ itself, so a parse unchanged since the last echo never re-posts.*" >/dev/null log "#$n: blockers closed -> ready" ;; esac elif has_issue_label epic; then + if has_issue_label release; then + refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" + cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" + states="$(reference_states <<<"$refs")" + if [ "$(blocked_decision "$refs" "$states" "$cross_refs")" = READY ]; then + ensure_comment "$n" release-init-due \ + "This release epic's declared gate is open. Release initialization is due: + +1. Mint the window's members. +2. Graph hard dependencies and same-file clusters. +3. Write ordered waves and the progress task list. +4. Ask the operator to bless the order, then open the first wave. +5. Ship the release, close this epic, and trigger the next window. + +See [\`RELEASES.md\`](https://github.com/$REPO/blob/main/RELEASES.md). The operator blessing the order is the one step this chain never automates." + log "#$n: release-init due" + fi + fi refs="$(epic_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" states="$(reference_states <<<"$refs")" if [ "$(epic_decision "$refs" "$states")" = NUDGE ]; then diff --git a/changelog.d/253.md b/changelog.d/253.md new file mode 100644 index 0000000..3bb9f9c --- /dev/null +++ b/changelog.d/253.md @@ -0,0 +1,3 @@ +### Added + +- Release epics now announce release initialization when their declared dependency gates clear. (#253) diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index a1b9486..fafa598 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -466,6 +466,69 @@ issue_probe() { # $1 issue, $2 labels, $3 assignees, $4 false|closing|refs, $5 m tfix() { printf '%s/repos_owner_repo_issues_%s_timeline.json' "$TMP" "$1"; } cfix() { printf '%s/repos_owner_repo_issues_%s_comments.json' "$TMP" "$1"; } +# -- release epics announce an opened declared gate, comment-only (#253) ----- +printf '{"state":"closed"}\n' >"$TMP/repos_owner_repo_issues_201.json" +printf '{"state":"closed"}\n' >"$TMP/repos_owner_repo_issues_202.json" +printf '{"state":"open"}\n' >"$TMP/repos_owner_repo_issues_203.json" +printf '[]\n' >"$(cfix 53)" +: >"$TMP/issue-edits" +release_init_edits_before="$(wc -l <"$TMP/issue-edits")" +release_init_body=$'Blocked by #201, #202.\n\n## Task list\n- [ ] #203 later work' +release_init="$(issue_probe 53 $'epic\nrelease' 0 false "" "$release_init_body")" +check "a release epic with every declared blocker closed announces init" 0 "" \ + grep -qF '' "$TMP/posted-53" +check "the init announce names all five steps" 0 "5" \ + grep -cE '^[1-5]\. ' "$TMP/posted-53" +check "the init announce cites RELEASES.md" 0 "" \ + grep -qF 'RELEASES.md' "$TMP/posted-53" +check "the init announce names the never-automated operator blessing" 0 "" \ + grep -qF 'operator blessing the order is the one step this chain never automates' \ + "$TMP/posted-53" +check "the opened release gate is logged" 0 "" \ + grep -qF '#53: release-init due' <<<"$release_init" +issue_probe 53 $'epic\nrelease' 0 false "" "$release_init_body" >/dev/null +check "an unchanged opened gate announces only once" 0 "1" \ + grep -cF '' "$TMP/posted-53" + +printf '[]\n' >"$(cfix 54)" +issue_probe 54 $'epic\nrelease' 0 false "" \ + $'Blocked by #203.\n\n## Task list\n- [x] #201 complete' >/dev/null +check "an open declared blocker suppresses init despite a complete task list" 1 "" \ + grep -qF '' "$TMP/posted-54" +check "the independent epic-complete nudge still fires" 0 "" \ + grep -qF '' "$TMP/posted-54" + +printf '[]\n' >"$(cfix 55)" +issue_probe 55 $'epic\nrelease' 0 false "" \ + $'Blocked by #201.\n\n## Task list\n- [x] #202 complete' >/dev/null +check "release-init and epic-complete can coexist" 0 "2" \ + grep -c -- '^----$' "$TMP/posted-55" +check "the coexisting comments keep distinct markers" 0 "" \ + bash -c 'grep -qF "" "$1" && grep -qF "" "$1"' \ + _ "$TMP/posted-55" + +for n in 56 57 58 59; do printf '[]\n' >"$(cfix "$n")"; done +issue_probe 56 $'epic\nrelease' 0 false "" 'No dependency declaration.' >/dev/null +check "a release epic without a Blocked by declaration stays silent" 1 "" \ + test -f "$TMP/posted-56" +issue_probe 57 $'epic\nrelease' 0 false "" 'Blocked by heavy-duty/rig#9.' >/dev/null +check "a cross-repo release gate stays silent" 1 "" test -f "$TMP/posted-57" +issue_probe 58 $'epic\nrelease' 0 false "" 'Blocked by #204.' >/dev/null +check "an unreadable release gate stays silent" 1 "" test -f "$TMP/posted-58" +: >"$TMP/api-calls" +issue_probe 59 epic 0 false "" 'Blocked by #201.' >/dev/null +check "a plain epic does not parse or announce a release gate" 1 "" \ + test -f "$TMP/posted-59" +check "a plain epic pays no Blocked by reference read" 1 "" \ + grep -qF 'repos/owner/repo/issues/201' "$TMP/api-calls" + +printf '[]\n' >"$(cfix 60)" +issue_probe 60 $'ready\nrelease' 0 false "" 'Blocked by #201.' >/dev/null +check "a non-epic release issue does not announce init" 1 "" \ + test -f "$TMP/posted-60" +check "every release-init probe is comment-only" 0 "$release_init_edits_before" \ + bash -c 'wc -l <"$1"' _ "$TMP/issue-edits" + # -- malformed attention targets are diagnosed, never repaired (#232) ------- attention_episode() { # $1 issue, $2 labeled timestamp jq -n --arg at "$2" \