
Eliteforge Feature Oriented Release Flow
- 13 installs
- Updated July 24, 2026
- cloudsen/eliteforge-skills
Stateful release workflow controller for EliteForge feature-oriented Git projects, advancing one release-boundary action at a time.
About
Drives the EliteForge feature-oriented Git release process as a strict state machine, handling iteration branches, dev/QA publishing, feature squashing, release merges, snapshot/formal deploys, and tagging. A developer uses it only for release-boundary actions, not ordinary coding.
- Advances one release workflow state at a time
- Scoped to boundary actions like QA submit, squash, release merge, deploy
Eliteforge Feature Oriented Release Flow by the numbers
- 13 all-time installs (skills.sh)
- Ranked #177 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cloudsen/eliteforge-skills --skill eliteforge-feature-oriented-release-flowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| Last updated | July 24, 2026 |
| Repository | cloudsen/eliteforge-skills ↗ |
What it does
Stateful release workflow controller for EliteForge feature-oriented Git projects, advancing one release-boundary action at a time.
Files
EliteForge Feature-Oriented Release Flow
Overview
Advance one release workflow state at a time for the full EliteForge Feature-Oriented Git process. Keep the state machine strict at release boundaries, but leave normal development work outside this skill.
Invocation Boundary
Always follow the EliteForge Git Feature-Oriented Spec. Use this skill only for workflow boundary actions:
- Initialize iteration branches.
- Publish feature work to dev through
nightly. - Submit, verify, or deploy QA through
qa/<version>. - Return from failed QA to feature fixes.
- Squash the feature branch after QA passes.
- Merge approved feature branches into
release/<version>. - Rebase/push release, deploy snapshot, tag, deploy formal version, and merge back to
master.
Do not use this skill for ordinary feature coding, routine commits, local tests, linting, formatting, dependency bumps, or generic Git questions.
Inputs
version: required release version, for example1.0.0.developer: required developer segment, for exampleclouds3n.taskName: required branch-safe task name, for exampledemo.taskId: optional task identifier, for example#123.multiAgent: optional mode. Use it only when the task has entered the EliteForge Git Feature-Oriented Spec multi-agent workflow.
When taskId exists, derive non multi-agent branch feature/<version>/<developer>/<taskId>/<taskName> and state file docs/tasks/release-flow/<version>/<taskId>-<taskName>.json.
When taskId is absent, derive non multi-agent branch feature/<version>/<developer>/<taskName> and state file docs/tasks/release-flow/<version>/<taskName>.json.
When multiAgent is active, the release-flow feature_branch is the matching multi-agent base branch: feature/<version>/<developer>[/<taskId>]/<taskName>/base. Subagent branches must already be merged into this /base branch before release-flow promotion states run.
Operating Rules
- Execute exactly one state transition per user request unless the user explicitly asks for a larger run.
- Before risky transitions, run
scripts/release_flow_state.sh --version <version> --developer <developer> --task-name <taskName>from the target repository; add--task-id <taskId>only when a task ID exists, and add--multi-agentonly when the release-flow feature branch must be.../<taskName>/base. - Use
references/release-flow-state-machine.mdwhen command details, transition guards, or failure handling are needed. - Persist workflow state in
docs/tasks/release-flow/<version>/<taskId>-<taskName>.json. This state file is required, not optional. taskIdis optional. IftaskIdis absent, persist workflow state indocs/tasks/release-flow/<version>/<taskName>.json.- Use the literal
taskIdandtaskNamein the state file name. Do not strip#from task IDs such as#123; quote the path in shell commands. Do not add a leading hyphen whentaskIdis absent. - Stop if
version,taskIdwhen present, ortaskNamecontains/, because each value must remain one path segment. - Do not use
/basefor non multi-agent release flows. If a task starts non multi-agent and later enters multi-agent mode, rename the existing feature branch to the matching/basebranch before creating or promoting subagent work. - Update the state file only after the command sequence and guard checks for the new state have succeeded. Use
scripts/persist_release_flow_state.pyfor the write.
Invariants
- Stop if
git status --porcelaincontains non-state-file changes before switching, merging, rebasing, tagging, deploying, or pushing, except when intentionally committing user changes. - Never skip MR approval before QA deployment.
- Never force push except after feature squash or release rebase.
- Never deploy from an ambiguous or wrong branch.
- Never continue automatically through merge conflicts, rebase conflicts, missing remote branches, existing tags, failed deploys, or unclear protected-branch rules.
- Treat
masterandmainas equivalent only for branch policy; follow the repository's actual default branch in commands. If this release flow namesmaster, verify whether the repo usesmasterormainbefore executing.
Compact State Table
| State | Expected branch | Boundary action | Next |
|---|---|---|---|
READY | any clean branch | Fetch, prune, verify default branch | INIT_BRANCHES |
INIT_BRANCHES | default branch refs | Create feature, qa, and release from default | FEATURE_DEV |
FEATURE_DEV | feature/... or feature/.../base | Commit normal feature or fix work | DEV_PUBLISH |
DEV_PUBLISH | nightly, then feature | Merge feature into nightly, push, deploy dev, push feature | QA_MR |
QA_MR | GitLab | Create or verify MR from feature to qa/<version> | QA_DEPLOY |
QA_DEPLOY | qa/<version> | Pull QA, deploy VERSION=<version>-SNAPSHOT | FEATURE_FIX or SQUASH |
FEATURE_FIX | feature/... or feature/.../base | Commit QA fix | DEV_PUBLISH |
SQUASH | feature/... or feature/.../base | Interactive rebase from merge-base, force push feature | RELEASE_MERGE |
RELEASE_MERGE | release/<version> | Merge approved squashed feature branches | RELEASE_REBASE |
RELEASE_REBASE | release/<version> | Rebase onto default remote, force push release | SNAPSHOT_DEPLOY |
SNAPSHOT_DEPLOY | release/<version> | Deploy VERSION=<version>-SNAPSHOT | TAG |
TAG | release/<version> | Create and push tag <version> | PROD_DEPLOY |
PROD_DEPLOY | release/<version> | Deploy VERSION=<version> | MASTER_BACKMERGE |
MASTER_BACKMERGE | default branch | Fast-forward merge release/<version> or create protected-branch MR | DONE |
Resources
references/release-flow-state-machine.md: Complete state machine, command templates, guards, and blocked states.scripts/release_flow_state.sh: Read-only Git diagnostic helper for current branch, cleanliness, derived branches, state file path, upstreams, remote refs, local tag existence, optional remote tag existence, and state hints.scripts/persist_release_flow_state.py: Deterministically create or update the required state JSON after a successful state transition.
interface:
display_name: "eliteforge-feature-oriented-release-flow"
short_description: "Drive EliteForge release states safely."
default_prompt: "Use $eliteforge-feature-oriented-release-flow to advance the current version release workflow."
EliteForge Feature-Oriented Release State Machine
This reference is the execution source for the full release workflow. Use it only after the lightweight SKILL.md decides that the current request is a release boundary action.
Input Contract
Required values:
version, example:1.0.0developer, example:clouds3ntaskName, example:demo
Optional value:
taskId, example:#123multiAgent, boolean. Use only after the task has entered the EliteForge Git Feature-Oriented Spec multi-agent workflow.
Derived values:
if [ -n "${taskId:-}" ]; then
task_branch_path="${version}/${developer}/${taskId}/${taskName}"
state_file="docs/tasks/release-flow/${version}/${taskId}-${taskName}.json"
else
task_branch_path="${version}/${developer}/${taskName}"
state_file="docs/tasks/release-flow/${version}/${taskName}.json"
fi
non_multi_agent_branch="feature/${task_branch_path}"
multi_agent_base_branch="${non_multi_agent_branch}/base"
if [ "${multiAgent:-false}" = "true" ]; then
feature_branch="$multi_agent_base_branch"
else
feature_branch="$non_multi_agent_branch"
fiAlways derive:
qa_branch="qa/${version}"
release_branch="release/${version}"
snapshot_version="${version}-SNAPSHOT"taskName must already be branch-safe. If it contains spaces or unsafe Git ref characters, stop and ask for the branch name instead of inventing one. Always quote branch variables because taskId can contain #. Do not use /base for non multi-agent release flows; in multi-agent mode, feature_branch is the /base integration branch and subagent branches must already be merged into it before promotion.
State File Contract
Persist current workflow state in the target repository at:
if [ -n "${taskId:-}" ]; then
state_file="docs/tasks/release-flow/${version}/${taskId}-${taskName}.json"
else
state_file="docs/tasks/release-flow/${version}/${taskName}.json"
fiThis file is required for repeated invocations. Use the literal taskId and taskName; for example taskId=#123 and taskName=demo produce:
docs/tasks/release-flow/1.0.0/#123-demo.jsonWhen taskId is absent, version=1.0.0 and taskName=demo produce:
docs/tasks/release-flow/1.0.0/demo.jsonRules:
- Do not strip
#from task IDs. - Do not add a leading hyphen when
taskIdis absent. - Quote the state file path in shell commands.
- Stop if
version,taskIdwhen present, ortaskNamecontains/. - Create the parent directory when it does not exist.
- Update this file only after a state transition succeeds and the real Git state agrees with the new state.
- Treat this exact state file as workflow metadata. It may be dirty while the next transition is being prepared; all other dirty paths remain blockers unless they are intentional user changes being committed.
Persist state with:
scripts/persist_release_flow_state.py \
--version "$version" \
--developer "$developer" \
--task-name "$taskName" \
--state "<NEW_STATE>" \
--previous-state "<PREVIOUS_STATE>"Add --task-id "$taskId" only when a task ID exists. Add --multi-agent only when the effective feature_branch is the matching /base branch.
Minimum JSON shape:
{
"version": "1.0.0",
"developer": "clouds3n",
"taskId": "#123",
"taskName": "demo",
"branchMode": "non_multi_agent",
"featureBranch": "feature/1.0.0/clouds3n/#123/demo",
"nonMultiAgentBranch": "feature/1.0.0/clouds3n/#123/demo",
"multiAgentBaseBranch": "feature/1.0.0/clouds3n/#123/demo/base",
"qaBranch": "qa/1.0.0",
"releaseBranch": "release/1.0.0",
"snapshotVersion": "1.0.0-SNAPSHOT",
"state": "FEATURE_DEV",
"stateFile": "docs/tasks/release-flow/1.0.0/#123-demo.json",
"updatedAt": "ISO-8601 UTC timestamp",
"git": {
"defaultBranch": "master-or-main",
"currentBranch": "current-branch",
"head": "current-commit",
"dirty": false
},
"history": []
}When no task ID exists, persist "taskId": null, use branch feature/<version>/<developer>/<taskName> in non multi-agent mode, use feature/<version>/<developer>/<taskName>/base in multi-agent mode, and use state file docs/tasks/release-flow/<version>/<taskName>.json.
Default Branch Rule
The historical flow says master. First detect the repository's actual default branch:
default_branch="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD | sed 's#^origin/##' || true)"
if [ -z "$default_branch" ]; then
default_branch="master"
fiUse origin/$default_branch and $default_branch in commands unless the project explicitly mandates master.
Global Guards
Before switching, merging, rebasing, tagging, deploying, or pushing, stop when there are dirty paths other than the matching state file:
if [ -n "${taskId:-}" ]; then
scripts/release_flow_state.sh --version "$version" --developer "$developer" --task-id "$taskId" --task-name "$taskName"
else
scripts/release_flow_state.sh --version "$version" --developer "$developer" --task-name "$taskName"
fiAdd --multi-agent to the diagnostic command only when feature_branch is the /base branch.
Stop on:
- Dirty non-state-file paths at a risky boundary.
- Detached HEAD.
- Missing default remote branch.
- Missing required local or remote branches.
- Merge conflict or rebase conflict.
- Existing local or remote tag for
version. - Failed deploy command.
- MR not approved or not merged.
- Protected branch rules that make direct push unclear.
State Transitions
READY -> INIT_BRANCHES
Guard:
- Repository is clean.
origin/<default_branch>exists.
Action:
git fetch -v -f -t --pruneINIT_BRANCHES -> FEATURE_DEV
Guard:
- Local
feature_branch,qa_branch, andrelease_branchdo not already exist, or the user explicitly asks to resume existing branches. origin/<default_branch>exists.
Action:
git switch -c "$feature_branch" --track "origin/$default_branch"
git switch -c "$qa_branch" --track "origin/$default_branch"
git switch -c "$release_branch" --track "origin/$default_branch"
git switch "$feature_branch"If GitLab MR must target qa_branch, ensure the remote branch exists:
git push -u origin "$qa_branch"The feature branch may still track origin/<default_branch> after creation. Before normal feature pushes, either verify upstream or push explicitly:
git push -u origin "$feature_branch"FEATURE_DEV -> DEV_PUBLISH
Allowed action when taskId exists:
git switch "$feature_branch"
git commit -m "feat(${taskId}): xxx"
git commit -m "fix(${taskId}): xxxx"Allowed action when taskId is absent:
git switch "$feature_branch"
git commit -m "feat: xxx"
git commit -m "fix: xxxx"Guard:
- At least one task commit exists on
feature_branchafter the branch base. - Unit/build checks required by the project have passed.
DEV_PUBLISH -> QA_MR
Purpose:
- Publish dev by merging the feature into
nightly. - Push the feature branch after dev publish.
Guard:
nightlyexists locally or remotely.- Working tree is clean.
Action:
git switch nightly
git merge "$feature_branch"
git push
make deploy
git switch "$feature_branch"
git push -u origin "$feature_branch"If nightly is protected or MR-only, stop and create the required MR instead of direct merge/push.
QA_MR -> QA_DEPLOY
External action:
- Create or verify GitLab MR.
- Source:
feature_branch - Target:
qa_branch - Assignee: self /
developer - Reviewer: responsible reviewer
Guard:
- MR is approved and merged.
origin/$qa_branchcontains the feature changes.
QA_DEPLOY -> FEATURE_FIX or SQUASH
Action:
git fetch -v -f -t --prune
git switch "$qa_branch"
git pull --ff-only origin "$qa_branch"
make deploy VERSION="$snapshot_version"Result:
- If QA fails, go to
FEATURE_FIX. - If QA passes, go to
SQUASH.
FEATURE_FIX -> DEV_PUBLISH
Action:
git switch "$feature_branch"
# apply fixes
if [ -n "${taskId:-}" ]; then
git commit -m "fix(${taskId}): xxxx"
else
git commit -m "fix: xxxx"
fiThen repeat dev publish, QA MR, and QA deploy.
SQUASH -> RELEASE_MERGE
Guard:
- QA has passed.
feature_branchcontains only this task's commits after its base commit.- No uncommitted changes.
Action:
git switch "$feature_branch"
base_commit="$(git merge-base "origin/$default_branch" "$feature_branch")"
git rebase -i "$base_commit"
git push --force --no-verify origin "$feature_branch"Never squash before QA passes.
RELEASE_MERGE -> RELEASE_REBASE
Guard:
- Every feature branch being merged has passed QA and has been squashed.
- Release branch is clean.
Action:
git switch "$release_branch"
git merge "$feature_branch"
git merge "feature/${version}/otherdeveloper/xxx"
git merge "feature/${version}/otherdeveloper/yyy"Use the actual approved branch list. Do not merge placeholder branch names.
RELEASE_REBASE -> SNAPSHOT_DEPLOY
Action:
git fetch -v -f -t --prune
git rebase "origin/$default_branch"
git push --force --no-verify origin "$release_branch"Stop on rebase conflicts.
SNAPSHOT_DEPLOY -> TAG
Action:
make deploy VERSION="$snapshot_version"Guard:
- Snapshot deployment succeeds.
TAG -> PROD_DEPLOY
Guard:
- Local tag
$versiondoes not exist. - Remote tag
$versiondoes not exist.
Action:
git tag "$version"
git push origin "$version"PROD_DEPLOY -> MASTER_BACKMERGE
Action:
make deploy VERSION="$version"Guard:
- Formal deployment succeeds.
MASTER_BACKMERGE -> DONE
Action for repositories allowing direct default-branch updates:
git switch "$default_branch"
git pull --ff-only origin "$default_branch"
git merge --ff-only "$release_branch"
git push origin "$default_branch"If the default branch is protected, create a release-to-default MR instead of pushing directly.
State Persistence
For repeated invocations, create or update docs/tasks/release-flow/<version>/<taskId>-<taskName>.json when taskId exists, otherwise docs/tasks/release-flow/<version>/<taskName>.json:
{
"version": "1.0.0",
"developer": "clouds3n",
"taskId": "#123",
"taskName": "demo",
"branchMode": "non_multi_agent",
"featureBranch": "feature/1.0.0/clouds3n/#123/demo",
"nonMultiAgentBranch": "feature/1.0.0/clouds3n/#123/demo",
"multiAgentBaseBranch": "feature/1.0.0/clouds3n/#123/demo/base",
"qaBranch": "qa/1.0.0",
"releaseBranch": "release/1.0.0",
"snapshotVersion": "1.0.0-SNAPSHOT",
"stateFile": "docs/tasks/release-flow/1.0.0/#123-demo.json",
"state": "FEATURE_DEV"
}Only update state after the transition has succeeded and the real Git state agrees with it.
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
def run_git(args):
try:
result = subprocess.run(
["git", *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return ""
def git_success(args):
return (
subprocess.run(
["git", *args],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
text=True,
).returncode
== 0
)
def require_git_repo():
if run_git(["rev-parse", "--is-inside-work-tree"]) != "true":
print("ERROR: current directory is not inside a Git work tree.", file=sys.stderr)
sys.exit(1)
def default_branch():
remote_head = run_git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"])
if remote_head.startswith("origin/"):
return remote_head.removeprefix("origin/")
if git_success(["show-ref", "--verify", "--quiet", "refs/remotes/origin/master"]):
return "master"
if git_success(["show-ref", "--verify", "--quiet", "refs/remotes/origin/main"]):
return "main"
return "master"
def validate_path_segment(name, value):
if not value:
print(f"ERROR: {name} must not be empty.", file=sys.stderr)
sys.exit(2)
if "/" in value or "\x00" in value:
print(f"ERROR: {name} must be one filename segment and must not contain '/'.", file=sys.stderr)
sys.exit(2)
def main():
parser = argparse.ArgumentParser(
description="Persist EliteForge feature-oriented release workflow state JSON."
)
parser.add_argument("--version", required=True)
parser.add_argument("--developer", required=True)
parser.add_argument("--task-id", default="")
parser.add_argument("--task-name", required=True)
parser.add_argument(
"--multi-agent",
action="store_true",
help="Persist featureBranch as feature/<version>/<developer>[/<taskId>]/<taskName>/base.",
)
parser.add_argument("--state", required=True)
parser.add_argument("--previous-state", default="")
parser.add_argument("--notes", default="")
args = parser.parse_args()
require_git_repo()
for name, value in (
("version", args.version),
("task-name", args.task_name),
):
validate_path_segment(name, value)
if args.task_id:
validate_path_segment("task-id", args.task_id)
if args.task_id:
task_branch_path = f"{args.version}/{args.developer}/{args.task_id}/{args.task_name}"
state_filename = f"{args.task_id}-{args.task_name}.json"
task_id_value = args.task_id
else:
task_branch_path = f"{args.version}/{args.developer}/{args.task_name}"
state_filename = f"{args.task_name}.json"
task_id_value = None
non_multi_agent_branch = f"feature/{task_branch_path}"
multi_agent_base_branch = f"{non_multi_agent_branch}/base"
branch_mode = "multi_agent" if args.multi_agent else "non_multi_agent"
feature_branch = multi_agent_base_branch if args.multi_agent else non_multi_agent_branch
qa_branch = f"qa/{args.version}"
release_branch = f"release/{args.version}"
snapshot_version = f"{args.version}-SNAPSHOT"
state_file = Path("docs/tasks/release-flow") / args.version / state_filename
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
git_info = {
"defaultBranch": default_branch(),
"currentBranch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "HEAD",
"head": run_git(["rev-parse", "HEAD"]),
"dirty": bool(run_git(["status", "--porcelain", "--untracked-files=all"])),
}
if state_file.exists():
try:
data = json.loads(state_file.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
print(f"ERROR: existing state file is not valid JSON: {state_file}: {exc}", file=sys.stderr)
sys.exit(1)
if not isinstance(data, dict):
print(f"ERROR: existing state file must contain a JSON object: {state_file}", file=sys.stderr)
sys.exit(1)
else:
data = {}
history = data.get("history", [])
if not isinstance(history, list):
history = []
data.pop("task" + "Desc", None)
history.append(
{
"state": args.state,
"previousState": args.previous_state,
"updatedAt": now,
"head": git_info["head"],
"currentBranch": git_info["currentBranch"],
"notes": args.notes,
}
)
data.update(
{
"version": args.version,
"developer": args.developer,
"taskId": task_id_value,
"taskName": args.task_name,
"branchMode": branch_mode,
"featureBranch": feature_branch,
"nonMultiAgentBranch": non_multi_agent_branch,
"multiAgentBaseBranch": multi_agent_base_branch,
"qaBranch": qa_branch,
"releaseBranch": release_branch,
"snapshotVersion": snapshot_version,
"state": args.state,
"stateFile": state_file.as_posix(),
"updatedAt": now,
"git": git_info,
"history": history,
}
)
state_file.parent.mkdir(parents=True, exist_ok=True)
state_file.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(state_file.as_posix())
if __name__ == "__main__":
main()
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
release_flow_state.sh --version <version> --developer <developer> [--task-id <taskId>] --task-name <taskName> [--multi-agent] [--check-remote-tags]
Read-only diagnostic helper for the EliteForge feature-oriented release flow.
Run it from the target Git repository before executing a release state transition.
Options:
--version Release version, for example 1.0.0
--developer Developer segment in the feature branch
--task-id Optional task id segment, for example #123
--task-name Branch-safe task name segment, for example demo
--multi-agent Use feature/<version>/<developer>[/<taskId>]/<taskName>/base
as the release-flow feature branch
--check-remote-tags
Also query origin for the version tag. Disabled by default to
keep frequent state checks fast and offline-friendly.
-h, --help Show this help
EOF
}
version=""
developer=""
task_id=""
task_name=""
check_remote_tags="false"
multi_agent="false"
while [ "$#" -gt 0 ]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--developer)
developer="${2:-}"
shift 2
;;
--task-id)
task_id="${2:-}"
shift 2
;;
--task-name)
task_name="${2:-}"
shift 2
;;
--check-remote-tags)
check_remote_tags="true"
shift
;;
--multi-agent)
multi_agent="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [ -z "$version" ] || [ -z "$developer" ] || [ -z "$task_name" ]; then
echo "ERROR: --version, --developer, and --task-name are required." >&2
usage >&2
exit 2
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "ERROR: current directory is not inside a Git work tree." >&2
exit 1
fi
if [ -n "$task_id" ]; then
task_branch_path="${version}/${developer}/${task_id}/${task_name}"
state_file="docs/tasks/release-flow/${version}/${task_id}-${task_name}.json"
else
task_branch_path="${version}/${developer}/${task_name}"
state_file="docs/tasks/release-flow/${version}/${task_name}.json"
fi
non_multi_agent_branch="feature/${task_branch_path}"
multi_agent_base_branch="${non_multi_agent_branch}/base"
if [ "$multi_agent" = "true" ]; then
branch_mode="multi_agent"
feature_branch="$multi_agent_base_branch"
else
branch_mode="non_multi_agent"
feature_branch="$non_multi_agent_branch"
fi
qa_branch="qa/${version}"
release_branch="release/${version}"
snapshot_version="${version}-SNAPSHOT"
default_branch="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##' || true)"
if [ -z "$default_branch" ]; then
if git show-ref --verify --quiet refs/remotes/origin/master; then
default_branch="master"
elif git show-ref --verify --quiet refs/remotes/origin/main; then
default_branch="main"
else
default_branch="master"
fi
fi
current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
dirty="false"
dirty_non_state="false"
status_output="$(git status --porcelain --untracked-files=all)"
if [ -n "$status_output" ]; then
dirty="true"
while IFS= read -r status_line; do
[ -z "$status_line" ] && continue
status_path="${status_line:3}"
if [ "$status_path" != "$state_file" ]; then
dirty_non_state="true"
break
fi
done <<< "$status_output"
fi
exists_local() {
git show-ref --verify --quiet "refs/heads/$1"
}
exists_remote() {
git show-ref --verify --quiet "refs/remotes/origin/$1"
}
ahead_count() {
local base="$1"
local head="$2"
if git rev-parse --verify "$base" >/dev/null 2>&1 && git rev-parse --verify "$head" >/dev/null 2>&1; then
git rev-list --count "${base}..${head}"
else
echo "unknown"
fi
}
upstream_of() {
local branch="$1"
git rev-parse --abbrev-ref "${branch}@{upstream}" 2>/dev/null || echo "none"
}
yaml_quote() {
printf "'"
printf "%s" "$1" | sed "s/'/''/g"
printf "'"
}
branch_format_error=""
for candidate_branch in "$feature_branch" "$qa_branch" "$release_branch"; do
if ! git check-ref-format --branch "$candidate_branch" >/dev/null 2>&1; then
branch_format_error="$candidate_branch"
break
fi
done
state_path_error=""
if [[ "$version" == */* ]]; then
state_path_error="version contains /"
elif [ -n "$task_id" ] && [[ "$task_id" == */* ]]; then
state_path_error="task-id contains /"
elif [[ "$task_name" == */* ]]; then
state_path_error="task-name contains /"
fi
state_hint="BLOCKED"
block_reason=""
if [ "$current_branch" = "HEAD" ]; then
block_reason="detached HEAD"
elif [ -n "$branch_format_error" ]; then
block_reason="invalid branch name: $branch_format_error"
elif [ -n "$state_path_error" ]; then
block_reason="invalid state file segment: $state_path_error"
elif [ "$dirty_non_state" = "true" ]; then
block_reason="dirty non-state-file paths"
elif ! exists_remote "$default_branch"; then
block_reason="missing origin/${default_branch}"
elif [ "$multi_agent" = "true" ] && [ "$current_branch" = "$non_multi_agent_branch" ]; then
block_reason="multi-agent release flow requires the /base branch; rename the non multi-agent branch first"
elif [ "$current_branch" = "$feature_branch" ]; then
commits="$(ahead_count "origin/${default_branch}" "$feature_branch")"
if [ "$commits" = "0" ]; then
state_hint="FEATURE_DEV"
else
state_hint="FEATURE_DEV_OR_READY_TO_PUBLISH"
fi
elif [ "$current_branch" = "nightly" ]; then
state_hint="DEV_PUBLISH"
elif [ "$current_branch" = "$qa_branch" ]; then
state_hint="QA_DEPLOY"
elif [ "$current_branch" = "$release_branch" ]; then
state_hint="RELEASE_MERGE_OR_RELEASE_DEPLOY"
elif [ "$current_branch" = "$default_branch" ]; then
state_hint="READY_OR_MASTER_BACKMERGE"
else
state_hint="UNKNOWN_BRANCH"
fi
local_tag="false"
remote_tag="false"
if git show-ref --tags --verify --quiet "refs/tags/$version"; then
local_tag="true"
fi
if [ "$check_remote_tags" = "true" ]; then
if git ls-remote --exit-code --tags origin "refs/tags/$version" >/dev/null 2>&1; then
remote_tag="true"
fi
else
remote_tag="not_checked"
fi
cat <<EOF
release_flow_state:
version: $(yaml_quote "$version")
developer: $(yaml_quote "$developer")
task_id: $(yaml_quote "$task_id")
task_name: $(yaml_quote "$task_name")
branch_mode: $(yaml_quote "$branch_mode")
default_branch: $(yaml_quote "$default_branch")
current_branch: $(yaml_quote "$current_branch")
dirty: $dirty
dirty_non_state: $dirty_non_state
state_hint: $(yaml_quote "$state_hint")
block_reason: $(yaml_quote "${block_reason:-none}")
branches:
feature: $(yaml_quote "$feature_branch")
non_multi_agent: $(yaml_quote "$non_multi_agent_branch")
multi_agent_base: $(yaml_quote "$multi_agent_base_branch")
qa: $(yaml_quote "$qa_branch")
release: $(yaml_quote "$release_branch")
snapshot_version: $(yaml_quote "$snapshot_version")
state_file: $(yaml_quote "$state_file")
existence:
origin_default: $(exists_remote "$default_branch" && echo true || echo false)
branch_names_valid: $([ -z "$branch_format_error" ] && echo true || echo false)
state_file_segment_valid: $([ -z "$state_path_error" ] && echo true || echo false)
local_feature: $(exists_local "$feature_branch" && echo true || echo false)
remote_feature: $(exists_remote "$feature_branch" && echo true || echo false)
local_non_multi_agent: $(exists_local "$non_multi_agent_branch" && echo true || echo false)
remote_non_multi_agent: $(exists_remote "$non_multi_agent_branch" && echo true || echo false)
local_multi_agent_base: $(exists_local "$multi_agent_base_branch" && echo true || echo false)
remote_multi_agent_base: $(exists_remote "$multi_agent_base_branch" && echo true || echo false)
local_qa: $(exists_local "$qa_branch" && echo true || echo false)
remote_qa: $(exists_remote "$qa_branch" && echo true || echo false)
local_release: $(exists_local "$release_branch" && echo true || echo false)
remote_release: $(exists_remote "$release_branch" && echo true || echo false)
upstreams:
feature: $(yaml_quote "$(exists_local "$feature_branch" && upstream_of "$feature_branch" || echo "missing")")
qa: $(yaml_quote "$(exists_local "$qa_branch" && upstream_of "$qa_branch" || echo "missing")")
release: $(yaml_quote "$(exists_local "$release_branch" && upstream_of "$release_branch" || echo "missing")")
tags:
local_version_tag: $local_tag
remote_version_tag: $(yaml_quote "$remote_tag")
counts:
feature_commits_after_origin_default: $(yaml_quote "$(ahead_count "origin/${default_branch}" "$feature_branch")")
EOF