@kody-ade/kody-engine 0.3.21 → 0.3.22
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.
- package/dist/bin/kody.js +189 -448
- package/dist/executables/release/profile.json +49 -51
- package/dist/executables/release-deploy/deploy.sh +84 -0
- package/dist/executables/release-deploy/profile.json +53 -0
- package/dist/executables/release-prepare/prepare.sh +328 -0
- package/dist/executables/release-prepare/profile.json +70 -0
- package/dist/executables/release-publish/profile.json +53 -0
- package/dist/executables/release-publish/publish.sh +110 -0
- package/dist/executables/research/prompt.md +11 -2
- package/package.json +1 -1
|
@@ -1,62 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "release",
|
|
3
|
-
"role": "
|
|
4
|
-
"
|
|
5
|
-
"describe": "Version bump + changelog + release PR (prepare), or tag + publish + GH release (finalize). No agent.",
|
|
3
|
+
"role": "orchestrator",
|
|
4
|
+
"describe": "Release orchestrator: drives release-prepare → merge PR → release-publish → release-deploy. No agent — postflight entries ARE the transition table, evaluated top-to-bottom via runWhen.",
|
|
6
5
|
"inputs": [
|
|
7
|
-
{
|
|
8
|
-
"name": "mode",
|
|
9
|
-
"flag": "--mode",
|
|
10
|
-
"type": "enum",
|
|
11
|
-
"values": [
|
|
12
|
-
"prepare",
|
|
13
|
-
"finalize"
|
|
14
|
-
],
|
|
15
|
-
"required": false,
|
|
16
|
-
"describe": "`prepare` (default): bump + changelog + release PR. `finalize`: E2E gate + tag + publish + GH release."
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"name": "bump",
|
|
20
|
-
"flag": "--bump",
|
|
21
|
-
"type": "enum",
|
|
22
|
-
"values": [
|
|
23
|
-
"patch",
|
|
24
|
-
"minor",
|
|
25
|
-
"major"
|
|
26
|
-
],
|
|
27
|
-
"required": false,
|
|
28
|
-
"describe": "Version bump when mode=prepare (ignored in finalize). Default patch."
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"name": "dry-run",
|
|
32
|
-
"flag": "--dry-run",
|
|
33
|
-
"type": "bool",
|
|
34
|
-
"required": false,
|
|
35
|
-
"describe": "Print plan without writing files, creating PRs, tagging, or publishing."
|
|
36
|
-
},
|
|
37
6
|
{
|
|
38
7
|
"name": "issue",
|
|
39
8
|
"flag": "--issue",
|
|
40
9
|
"type": "int",
|
|
41
|
-
"required":
|
|
42
|
-
"describe": "
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "prefer",
|
|
46
|
-
"flag": "--prefer",
|
|
47
|
-
"type": "enum",
|
|
48
|
-
"values": [
|
|
49
|
-
"ours",
|
|
50
|
-
"theirs"
|
|
51
|
-
],
|
|
52
|
-
"required": false,
|
|
53
|
-
"describe": "On release/vX.Y.Z branch collision (prepare mode): 'ours' force-pushes over the remote branch; 'theirs' reuses the existing branch and its open PR. Default (unset): refuse on non-ff."
|
|
10
|
+
"required": true,
|
|
11
|
+
"describe": "GitHub issue number to drive the release flow on."
|
|
54
12
|
}
|
|
55
13
|
],
|
|
56
14
|
"claudeCode": {
|
|
57
15
|
"model": "inherit",
|
|
58
|
-
"permissionMode": "
|
|
59
|
-
"maxTurns":
|
|
16
|
+
"permissionMode": "default",
|
|
17
|
+
"maxTurns": 0,
|
|
18
|
+
"maxThinkingTokens": null,
|
|
60
19
|
"systemPromptAppend": null,
|
|
61
20
|
"tools": [],
|
|
62
21
|
"hooks": [],
|
|
@@ -67,12 +26,51 @@
|
|
|
67
26
|
"mcpServers": []
|
|
68
27
|
},
|
|
69
28
|
"cliTools": [],
|
|
29
|
+
"inputArtifacts": [],
|
|
30
|
+
"outputArtifacts": [],
|
|
70
31
|
"scripts": {
|
|
71
32
|
"preflight": [
|
|
72
33
|
{
|
|
73
|
-
"script": "
|
|
74
|
-
|
|
34
|
+
"script": "setLifecycleLabel",
|
|
35
|
+
"with": {
|
|
36
|
+
"label": "kody-flow:release",
|
|
37
|
+
"color": "5319e7",
|
|
38
|
+
"description": "kody flow: release"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"script": "setLifecycleLabel",
|
|
43
|
+
"with": {
|
|
44
|
+
"label": "kody:orchestrating",
|
|
45
|
+
"color": "1d76db",
|
|
46
|
+
"description": "kody: orchestrating a multi-stage flow"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{ "script": "loadIssueContext" },
|
|
50
|
+
{ "script": "loadTaskState" },
|
|
51
|
+
{ "script": "skipAgent" }
|
|
75
52
|
],
|
|
76
|
-
"postflight": [
|
|
53
|
+
"postflight": [
|
|
54
|
+
{ "script": "startFlow", "with": { "entry": "release-prepare", "target": "issue" } },
|
|
55
|
+
|
|
56
|
+
{ "script": "mergeReleasePr",
|
|
57
|
+
"runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_PREPARE_COMPLETED" } },
|
|
58
|
+
|
|
59
|
+
{ "script": "dispatch", "with": { "next": "release-publish", "target": "issue" },
|
|
60
|
+
"runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_MERGE_COMPLETED" } },
|
|
61
|
+
|
|
62
|
+
{ "script": "dispatch", "with": { "next": "release-deploy", "target": "issue" },
|
|
63
|
+
"runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_PUBLISH_COMPLETED" } },
|
|
64
|
+
|
|
65
|
+
{ "script": "finishFlow",
|
|
66
|
+
"with": { "reason": "release-completed", "label": "kody:done", "color": "0e8a16", "description": "kody: release complete" },
|
|
67
|
+
"runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_DEPLOY_COMPLETED" } },
|
|
68
|
+
|
|
69
|
+
{ "script": "finishFlow",
|
|
70
|
+
"with": { "reason": "release-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: release flow failed" },
|
|
71
|
+
"runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_PREPARE_FAILED", "RELEASE_MERGE_FAILED", "RELEASE_PUBLISH_FAILED", "RELEASE_DEPLOY_FAILED"] } },
|
|
72
|
+
|
|
73
|
+
{ "script": "persistFlowState" }
|
|
74
|
+
]
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# release-deploy: run the configured deployCommand and/or notifyCommand
|
|
4
|
+
# after release-publish has tagged and published the artifact. No agent.
|
|
5
|
+
#
|
|
6
|
+
# Both commands are optional. With neither set, deploy is a no-op success
|
|
7
|
+
# (the orchestrator still advances to "done").
|
|
8
|
+
#
|
|
9
|
+
# Inputs (env):
|
|
10
|
+
# KODY_ARG_DRY_RUN true|false
|
|
11
|
+
# KODY_ARG_ISSUE triggering issue/PR number (optional)
|
|
12
|
+
#
|
|
13
|
+
# Config (env):
|
|
14
|
+
# KODY_CFG_RELEASE_DEPLOYCOMMAND optional; $VERSION substituted
|
|
15
|
+
# KODY_CFG_RELEASE_NOTIFYCOMMAND optional; $VERSION substituted
|
|
16
|
+
# KODY_CFG_RELEASE_TIMEOUTMS per-command timeout in ms (default 600000)
|
|
17
|
+
#
|
|
18
|
+
# Stdout signals:
|
|
19
|
+
# KODY_REASON=<text>
|
|
20
|
+
# KODY_SKIP_AGENT=true
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
dry_run="${KODY_ARG_DRY_RUN:-false}"
|
|
25
|
+
deploy_cmd="${KODY_CFG_RELEASE_DEPLOYCOMMAND:-}"
|
|
26
|
+
notify_cmd="${KODY_CFG_RELEASE_NOTIFYCOMMAND:-}"
|
|
27
|
+
timeout_ms="${KODY_CFG_RELEASE_TIMEOUTMS:-600000}"
|
|
28
|
+
timeout_s=$((timeout_ms / 1000))
|
|
29
|
+
|
|
30
|
+
read_pkg_version() {
|
|
31
|
+
python3 -c "import json; print(json.load(open('package.json'))['version'])"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if [[ ! -f package.json ]]; then
|
|
35
|
+
echo "KODY_REASON=release deploy: package.json not found"
|
|
36
|
+
echo "KODY_SKIP_AGENT=true"
|
|
37
|
+
exit 99
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
version=$(read_pkg_version)
|
|
41
|
+
echo "→ release deploy: v${version}"
|
|
42
|
+
|
|
43
|
+
if [[ -z "$deploy_cmd" && -z "$notify_cmd" ]]; then
|
|
44
|
+
echo "KODY_REASON=no deployCommand or notifyCommand configured — nothing to run"
|
|
45
|
+
echo "KODY_SKIP_AGENT=true"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
50
|
+
echo "KODY_REASON=dry-run — would run deploy/notify commands"
|
|
51
|
+
echo "KODY_SKIP_AGENT=true"
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
export HUSKY=0 SKIP_HOOKS=1 CI="${CI:-1}"
|
|
56
|
+
|
|
57
|
+
deploy_status="skipped"
|
|
58
|
+
if [[ -n "$deploy_cmd" ]]; then
|
|
59
|
+
cmd="${deploy_cmd//\$VERSION/$version}"
|
|
60
|
+
echo " deploy: ${cmd}"
|
|
61
|
+
if timeout "${timeout_s}" bash -c "$cmd"; then
|
|
62
|
+
deploy_status="ok"
|
|
63
|
+
else
|
|
64
|
+
deploy_status="failed"
|
|
65
|
+
echo "KODY_REASON=release deploy: deployCommand failed"
|
|
66
|
+
echo "KODY_SKIP_AGENT=true"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
notify_status="skipped"
|
|
72
|
+
if [[ -n "$notify_cmd" ]]; then
|
|
73
|
+
cmd="${notify_cmd//\$VERSION/$version}"
|
|
74
|
+
echo " notify: ${cmd}"
|
|
75
|
+
if timeout "${timeout_s}" bash -c "$cmd"; then
|
|
76
|
+
notify_status="ok"
|
|
77
|
+
else
|
|
78
|
+
notify_status="failed"
|
|
79
|
+
echo "[kody release-deploy] notifyCommand failed (non-fatal)" >&2
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo "KODY_REASON=deploy=${deploy_status} notify=${notify_status}"
|
|
84
|
+
echo "KODY_SKIP_AGENT=true"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "release-deploy",
|
|
3
|
+
"role": "utility",
|
|
4
|
+
"phase": "shipped",
|
|
5
|
+
"describe": "Run the configured deployCommand and notifyCommand after release-publish has tagged + published. No agent.",
|
|
6
|
+
"inputs": [
|
|
7
|
+
{
|
|
8
|
+
"name": "dry-run",
|
|
9
|
+
"flag": "--dry-run",
|
|
10
|
+
"type": "bool",
|
|
11
|
+
"required": false,
|
|
12
|
+
"describe": "Print plan without running deploy/notify commands."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "issue",
|
|
16
|
+
"flag": "--issue",
|
|
17
|
+
"type": "int",
|
|
18
|
+
"required": false,
|
|
19
|
+
"describe": "Issue/PR number to post the terminal notice on. Auto-injected by dispatch."
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"claudeCode": {
|
|
23
|
+
"model": "inherit",
|
|
24
|
+
"permissionMode": "acceptEdits",
|
|
25
|
+
"maxTurns": 0,
|
|
26
|
+
"maxThinkingTokens": null,
|
|
27
|
+
"systemPromptAppend": null,
|
|
28
|
+
"tools": [],
|
|
29
|
+
"hooks": [],
|
|
30
|
+
"skills": [],
|
|
31
|
+
"commands": [],
|
|
32
|
+
"subagents": [],
|
|
33
|
+
"plugins": [],
|
|
34
|
+
"mcpServers": []
|
|
35
|
+
},
|
|
36
|
+
"cliTools": [],
|
|
37
|
+
"inputArtifacts": [],
|
|
38
|
+
"outputArtifacts": [],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"preflight": [
|
|
41
|
+
{ "script": "setCommentTarget", "with": { "type": "issue" } },
|
|
42
|
+
{ "script": "loadTaskState" },
|
|
43
|
+
{ "shell": "deploy.sh" },
|
|
44
|
+
{ "script": "skipAgent" }
|
|
45
|
+
],
|
|
46
|
+
"postflight": [
|
|
47
|
+
{ "script": "recordOutcome" },
|
|
48
|
+
{ "script": "saveTaskState" },
|
|
49
|
+
{ "script": "notifyTerminal", "with": { "label": "release deploy" } },
|
|
50
|
+
{ "script": "advanceFlow" }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# release-prepare: bump version files, generate CHANGELOG.md, commit on a
|
|
4
|
+
# release branch, open the release PR. Pure mechanical work — no agent.
|
|
5
|
+
#
|
|
6
|
+
# Inputs (env, set by the executor):
|
|
7
|
+
# KODY_ARG_BUMP patch|minor|major (default: patch)
|
|
8
|
+
# KODY_ARG_DRY_RUN true|false
|
|
9
|
+
# KODY_ARG_PREFER ours|theirs (optional)
|
|
10
|
+
# KODY_ARG_ISSUE triggering issue/PR number (optional)
|
|
11
|
+
#
|
|
12
|
+
# Config (env, flattened from kody.config.json):
|
|
13
|
+
# KODY_CFG_GIT_DEFAULTBRANCH e.g. main
|
|
14
|
+
# KODY_CFG_RELEASE_VERSIONFILES JSON array, e.g. ["package.json"]
|
|
15
|
+
#
|
|
16
|
+
# Stdout signals to the executor:
|
|
17
|
+
# KODY_PR_URL=<url> — set ctx.output.prUrl (final PR URL)
|
|
18
|
+
# KODY_REASON=<text> — set ctx.output.reason (failure or dry-run note)
|
|
19
|
+
# KODY_SKIP_AGENT=true — bypass the agent (always; this is a no-agent flow)
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
# ── Helpers ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
bump="${KODY_ARG_BUMP:-patch}"
|
|
26
|
+
dry_run="${KODY_ARG_DRY_RUN:-false}"
|
|
27
|
+
prefer="${KODY_ARG_PREFER:-}"
|
|
28
|
+
default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-main}"
|
|
29
|
+
version_files_json="${KODY_CFG_RELEASE_VERSIONFILES:-}"
|
|
30
|
+
|
|
31
|
+
fail() {
|
|
32
|
+
local reason="$1"
|
|
33
|
+
echo "KODY_REASON=$reason"
|
|
34
|
+
echo "KODY_SKIP_AGENT=true"
|
|
35
|
+
exit "${2:-1}"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
bump_version() {
|
|
39
|
+
local cur="$1" kind="$2"
|
|
40
|
+
local rest="${cur#*-}"
|
|
41
|
+
local core="${cur%%-*}"
|
|
42
|
+
if ! [[ "$core" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
|
43
|
+
fail "release prepare: cannot parse version '$cur' (expected x.y.z[-suffix])" 99
|
|
44
|
+
fi
|
|
45
|
+
local maj="${BASH_REMATCH[1]}" min="${BASH_REMATCH[2]}" pat="${BASH_REMATCH[3]}"
|
|
46
|
+
case "$kind" in
|
|
47
|
+
major) maj=$((maj + 1)); min=0; pat=0 ;;
|
|
48
|
+
minor) min=$((min + 1)); pat=0 ;;
|
|
49
|
+
patch|*) pat=$((pat + 1)) ;;
|
|
50
|
+
esac
|
|
51
|
+
echo "${maj}.${min}.${pat}"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
read_pkg_version() {
|
|
55
|
+
python3 -c "import json,sys; print(json.load(open('package.json'))['version'])"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
write_pkg_version() {
|
|
59
|
+
local file="$1" new="$2"
|
|
60
|
+
python3 - "$file" "$new" <<'PY'
|
|
61
|
+
import json, sys
|
|
62
|
+
path, new = sys.argv[1], sys.argv[2]
|
|
63
|
+
try:
|
|
64
|
+
with open(path) as f:
|
|
65
|
+
text = f.read()
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
print("MISSING")
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(text)
|
|
71
|
+
except Exception:
|
|
72
|
+
print("UNCHANGED")
|
|
73
|
+
sys.exit(0)
|
|
74
|
+
if data.get("version") == new:
|
|
75
|
+
print("UNCHANGED")
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
data["version"] = new
|
|
78
|
+
indent = 2
|
|
79
|
+
with open(path, "w") as f:
|
|
80
|
+
f.write(json.dumps(data, indent=indent) + "\n")
|
|
81
|
+
print("WROTE")
|
|
82
|
+
PY
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resolve_version_files() {
|
|
86
|
+
if [[ -z "$version_files_json" ]]; then
|
|
87
|
+
echo "package.json"
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
python3 - <<PY
|
|
91
|
+
import json, os, sys
|
|
92
|
+
raw = os.environ.get("KODY_CFG_RELEASE_VERSIONFILES", "")
|
|
93
|
+
try:
|
|
94
|
+
arr = json.loads(raw)
|
|
95
|
+
except Exception:
|
|
96
|
+
print("package.json")
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
if isinstance(arr, list) and arr:
|
|
99
|
+
for f in arr:
|
|
100
|
+
if isinstance(f, str) and f:
|
|
101
|
+
print(f)
|
|
102
|
+
else:
|
|
103
|
+
print("package.json")
|
|
104
|
+
PY
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
generate_changelog() {
|
|
108
|
+
local new_version="$1"
|
|
109
|
+
local last_tag
|
|
110
|
+
if last_tag=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null); then
|
|
111
|
+
range="${last_tag}..HEAD"
|
|
112
|
+
git log "$range" --pretty=format:'%s||%h' --no-merges 2>/dev/null || true
|
|
113
|
+
else
|
|
114
|
+
git log -n100 HEAD --pretty=format:'%s||%h' --no-merges 2>/dev/null || true
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
format_changelog() {
|
|
119
|
+
local new_version="$1"
|
|
120
|
+
local raw="$2"
|
|
121
|
+
local date_str
|
|
122
|
+
date_str=$(date -u +%Y-%m-%d)
|
|
123
|
+
python3 - "$new_version" "$date_str" <<PY
|
|
124
|
+
import sys, re
|
|
125
|
+
new_version, date_str = sys.argv[1], sys.argv[2]
|
|
126
|
+
raw = sys.stdin.read()
|
|
127
|
+
buckets = {k: [] for k in ("feat", "fix", "perf", "refactor", "docs", "chore", "other")}
|
|
128
|
+
for line in raw.splitlines():
|
|
129
|
+
line = line.strip()
|
|
130
|
+
if not line:
|
|
131
|
+
continue
|
|
132
|
+
if "||" not in line:
|
|
133
|
+
continue
|
|
134
|
+
subject, sha = line.split("||", 1)
|
|
135
|
+
if re.match(r"(?i)^chore:\s*release\s+v\d", subject):
|
|
136
|
+
continue
|
|
137
|
+
m = re.match(r"^(\w+)(?:\(.*?\))?\s*:\s*(.+)$", subject)
|
|
138
|
+
if m:
|
|
139
|
+
kind = m.group(1).lower()
|
|
140
|
+
msg = m.group(2)
|
|
141
|
+
else:
|
|
142
|
+
kind = "other"
|
|
143
|
+
msg = subject
|
|
144
|
+
buckets.setdefault(kind, buckets["other"]).append(f"- {msg} ({sha})")
|
|
145
|
+
labels = [
|
|
146
|
+
("feat", "Features"),
|
|
147
|
+
("fix", "Fixes"),
|
|
148
|
+
("perf", "Performance"),
|
|
149
|
+
("refactor", "Refactoring"),
|
|
150
|
+
("docs", "Docs"),
|
|
151
|
+
("chore", "Chores"),
|
|
152
|
+
("other", "Other"),
|
|
153
|
+
]
|
|
154
|
+
parts = [f"## v{new_version} — {date_str}", ""]
|
|
155
|
+
emitted = False
|
|
156
|
+
for key, label in labels:
|
|
157
|
+
items = buckets.get(key) or []
|
|
158
|
+
if not items:
|
|
159
|
+
continue
|
|
160
|
+
parts.append(f"### {label}")
|
|
161
|
+
parts.extend(items)
|
|
162
|
+
parts.append("")
|
|
163
|
+
emitted = True
|
|
164
|
+
if not emitted:
|
|
165
|
+
parts.append("_No notable commits since the last release._")
|
|
166
|
+
parts.append("")
|
|
167
|
+
sys.stdout.write("\n".join(parts))
|
|
168
|
+
PY
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
prepend_changelog() {
|
|
172
|
+
local entry="$1"
|
|
173
|
+
local header='# Changelog
|
|
174
|
+
|
|
175
|
+
All notable changes to this project will be documented in this file.
|
|
176
|
+
|
|
177
|
+
'
|
|
178
|
+
if [[ -f CHANGELOG.md ]]; then
|
|
179
|
+
if grep -qE '^#\s*Changelog\b' CHANGELOG.md; then
|
|
180
|
+
python3 - "$entry" <<'PY'
|
|
181
|
+
import sys
|
|
182
|
+
entry = sys.argv[1]
|
|
183
|
+
with open("CHANGELOG.md") as f:
|
|
184
|
+
prior = f.read()
|
|
185
|
+
idx = prior.index("\n", prior.index("# Changelog"))
|
|
186
|
+
new = prior[: idx + 1] + "\n" + entry + prior[idx + 1 :]
|
|
187
|
+
with open("CHANGELOG.md", "w") as f:
|
|
188
|
+
f.write(new)
|
|
189
|
+
PY
|
|
190
|
+
else
|
|
191
|
+
python3 - "$entry" <<'PY'
|
|
192
|
+
import sys
|
|
193
|
+
entry = sys.argv[1]
|
|
194
|
+
header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n"
|
|
195
|
+
with open("CHANGELOG.md") as f:
|
|
196
|
+
prior = f.read()
|
|
197
|
+
with open("CHANGELOG.md", "w") as f:
|
|
198
|
+
f.write(header + entry + prior)
|
|
199
|
+
PY
|
|
200
|
+
fi
|
|
201
|
+
else
|
|
202
|
+
python3 - "$entry" <<'PY'
|
|
203
|
+
import sys
|
|
204
|
+
entry = sys.argv[1]
|
|
205
|
+
header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n"
|
|
206
|
+
with open("CHANGELOG.md", "w") as f:
|
|
207
|
+
f.write(header + entry)
|
|
208
|
+
PY
|
|
209
|
+
fi
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
remote_branch_exists() {
|
|
213
|
+
local branch="$1"
|
|
214
|
+
git ls-remote --heads origin "$branch" 2>/dev/null | grep -q .
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
find_open_pr() {
|
|
218
|
+
local branch="$1"
|
|
219
|
+
gh pr list --head "$branch" --state open --json url --limit 1 2>/dev/null \
|
|
220
|
+
| python3 -c 'import json,sys; data=json.load(sys.stdin); print(data[0]["url"] if data else "")' 2>/dev/null \
|
|
221
|
+
|| echo ""
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# ── Flow ───────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
if [[ ! -f package.json ]]; then
|
|
227
|
+
fail "release prepare: package.json not found" 99
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
old_version=$(read_pkg_version)
|
|
231
|
+
new_version=$(bump_version "$old_version" "$bump")
|
|
232
|
+
tag="v${new_version}"
|
|
233
|
+
release_branch="release/${tag}"
|
|
234
|
+
|
|
235
|
+
echo "→ release prepare: ${old_version} → ${new_version} (${bump})"
|
|
236
|
+
|
|
237
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
238
|
+
echo "RELEASE_PLAN=bump=${new_version} tag=${tag}"
|
|
239
|
+
echo "KODY_REASON=dry-run — would bump to ${new_version}${prefer:+ (--prefer ${prefer})}"
|
|
240
|
+
echo "KODY_SKIP_AGENT=true"
|
|
241
|
+
exit 0
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Branch-collision gate.
|
|
245
|
+
collides=false
|
|
246
|
+
if remote_branch_exists "$release_branch"; then
|
|
247
|
+
collides=true
|
|
248
|
+
case "$prefer" in
|
|
249
|
+
theirs)
|
|
250
|
+
existing=$(find_open_pr "$release_branch")
|
|
251
|
+
if [[ -n "$existing" ]]; then
|
|
252
|
+
echo " reusing existing PR (--prefer theirs): ${existing}"
|
|
253
|
+
echo "KODY_PR_URL=${existing}"
|
|
254
|
+
echo "KODY_REASON=reused existing release PR"
|
|
255
|
+
echo "KODY_SKIP_AGENT=true"
|
|
256
|
+
exit 0
|
|
257
|
+
fi
|
|
258
|
+
fail "release prepare --prefer theirs: ${release_branch} exists on remote but has no open PR — nothing to reuse" 4
|
|
259
|
+
;;
|
|
260
|
+
ours)
|
|
261
|
+
echo " branch ${release_branch} exists on remote — will force-push (--prefer ours)"
|
|
262
|
+
;;
|
|
263
|
+
*)
|
|
264
|
+
fail "release prepare: branch ${release_branch} already exists on remote. Use --prefer ours to force-push, or --prefer theirs to reuse the existing PR." 4
|
|
265
|
+
;;
|
|
266
|
+
esac
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Bump version files.
|
|
270
|
+
mapfile -t files < <(resolve_version_files)
|
|
271
|
+
touched=()
|
|
272
|
+
for f in "${files[@]}"; do
|
|
273
|
+
res=$(write_pkg_version "$f" "$new_version")
|
|
274
|
+
if [[ "$res" == "WROTE" ]]; then
|
|
275
|
+
touched+=("$f")
|
|
276
|
+
fi
|
|
277
|
+
done
|
|
278
|
+
if [[ ${#touched[@]} -eq 0 ]]; then
|
|
279
|
+
fail "release prepare: no version strings updated (files: ${files[*]})"
|
|
280
|
+
fi
|
|
281
|
+
echo " wrote ${touched[*]}"
|
|
282
|
+
|
|
283
|
+
# Changelog.
|
|
284
|
+
raw_log=$(generate_changelog "$new_version") || raw_log=""
|
|
285
|
+
entry=$(printf '%s' "$raw_log" | format_changelog "$new_version")
|
|
286
|
+
prepend_changelog "$entry"
|
|
287
|
+
echo " wrote CHANGELOG.md"
|
|
288
|
+
|
|
289
|
+
# Commit + push.
|
|
290
|
+
export HUSKY=0 SKIP_HOOKS=1
|
|
291
|
+
git checkout -b "$release_branch"
|
|
292
|
+
for f in "${touched[@]}" CHANGELOG.md; do
|
|
293
|
+
git add -- "$f"
|
|
294
|
+
done
|
|
295
|
+
git -c commit.gpgsign=false commit -m "chore: release ${tag}"
|
|
296
|
+
if [[ "$collides" == "true" && "$prefer" == "ours" ]]; then
|
|
297
|
+
git push -u --force-with-lease origin "$release_branch"
|
|
298
|
+
else
|
|
299
|
+
git push -u origin "$release_branch"
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# Open PR (or link to existing one if --prefer ours collided).
|
|
303
|
+
pr_url=""
|
|
304
|
+
if [[ "$collides" == "true" && "$prefer" == "ours" ]]; then
|
|
305
|
+
pr_url=$(find_open_pr "$release_branch")
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
if [[ -z "$pr_url" ]]; then
|
|
309
|
+
body_max=60000
|
|
310
|
+
if [[ ${#entry} -gt $body_max ]]; then
|
|
311
|
+
body_entry="${entry:0:$body_max}
|
|
312
|
+
|
|
313
|
+
_… truncated; see CHANGELOG.md_"
|
|
314
|
+
else
|
|
315
|
+
body_entry="$entry"
|
|
316
|
+
fi
|
|
317
|
+
body=$'Automated release PR opened by kody.\n\n'"$body_entry"$'\n\nMerge this and then run `kody release --mode finalize`.'
|
|
318
|
+
pr_url=$(printf '%s' "$body" | gh pr create --head "$release_branch" --base "$default_branch" --title "chore: release ${tag}" --body-file -)
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
if [[ -z "$pr_url" ]]; then
|
|
322
|
+
fail "release prepare: gh pr create returned empty URL" 4
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
echo "RELEASE_PR=${pr_url}"
|
|
326
|
+
echo "KODY_PR_URL=${pr_url}"
|
|
327
|
+
echo "KODY_REASON=opened release PR for ${tag}"
|
|
328
|
+
echo "KODY_SKIP_AGENT=true"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "release-prepare",
|
|
3
|
+
"role": "utility",
|
|
4
|
+
"phase": "shipped",
|
|
5
|
+
"describe": "Bump version files, generate CHANGELOG.md, commit on a release branch, open the release PR. No agent.",
|
|
6
|
+
"inputs": [
|
|
7
|
+
{
|
|
8
|
+
"name": "bump",
|
|
9
|
+
"flag": "--bump",
|
|
10
|
+
"type": "enum",
|
|
11
|
+
"values": ["patch", "minor", "major"],
|
|
12
|
+
"required": false,
|
|
13
|
+
"describe": "Version bump increment. Default patch."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "dry-run",
|
|
17
|
+
"flag": "--dry-run",
|
|
18
|
+
"type": "bool",
|
|
19
|
+
"required": false,
|
|
20
|
+
"describe": "Print plan without writing files, committing, or opening a PR."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "prefer",
|
|
24
|
+
"flag": "--prefer",
|
|
25
|
+
"type": "enum",
|
|
26
|
+
"values": ["ours", "theirs"],
|
|
27
|
+
"required": false,
|
|
28
|
+
"describe": "On release/vX.Y.Z branch collision: 'ours' force-pushes; 'theirs' reuses the existing PR. Default refuses non-ff."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "issue",
|
|
32
|
+
"flag": "--issue",
|
|
33
|
+
"type": "int",
|
|
34
|
+
"required": false,
|
|
35
|
+
"describe": "Issue/PR number to post the terminal notice on. Auto-injected by dispatch."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"claudeCode": {
|
|
39
|
+
"model": "inherit",
|
|
40
|
+
"permissionMode": "acceptEdits",
|
|
41
|
+
"maxTurns": 0,
|
|
42
|
+
"maxThinkingTokens": null,
|
|
43
|
+
"systemPromptAppend": null,
|
|
44
|
+
"tools": [],
|
|
45
|
+
"hooks": [],
|
|
46
|
+
"skills": [],
|
|
47
|
+
"commands": [],
|
|
48
|
+
"subagents": [],
|
|
49
|
+
"plugins": [],
|
|
50
|
+
"mcpServers": []
|
|
51
|
+
},
|
|
52
|
+
"cliTools": [],
|
|
53
|
+
"inputArtifacts": [],
|
|
54
|
+
"outputArtifacts": [],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"preflight": [
|
|
57
|
+
{ "script": "setCommentTarget", "with": { "type": "issue" } },
|
|
58
|
+
{ "script": "loadTaskState" },
|
|
59
|
+
{ "shell": "prepare.sh" },
|
|
60
|
+
{ "script": "skipAgent" }
|
|
61
|
+
],
|
|
62
|
+
"postflight": [
|
|
63
|
+
{ "script": "recordOutcome" },
|
|
64
|
+
{ "script": "saveTaskState" },
|
|
65
|
+
{ "script": "mirrorStateToPr" },
|
|
66
|
+
{ "script": "notifyTerminal", "with": { "label": "release prepare" } },
|
|
67
|
+
{ "script": "advanceFlow" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|