@jenga-ai/agent 1.1.0 → 1.1.1
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/README.md +7 -3
- package/agents/developer.md +82 -2
- package/agents/scrum-master.md +140 -21
- package/agents/tester.md +90 -8
- package/hooks/on_session_end.sh +171 -20
- package/package.json +1 -1
- package/scripts/check-permission-level.sh +107 -0
- package/scripts/check-publicignore-match.sh +122 -0
- package/scripts/check-worktree-liveness.sh +193 -0
- package/scripts/generate-rapport-manifest.sh +43 -0
- package/scripts/idea_manager.sh +47 -0
- package/scripts/install-worktree-commit-guard.sh +134 -0
- package/scripts/jenga-permission-level-switch.sh +109 -0
- package/scripts/smoke-harness.sh +139 -0
- package/scripts/validate-board.sh +62 -0
- package/scripts/with-lock.sh +158 -0
- package/scripts/worktree-remove-guard.sh +204 -0
- package/skills/clearify/SKILL.md +52 -0
- package/skills/commit/SKILL.md +13 -4
- package/skills/distribute/CONFIG_SCHEMA.md +60 -2
- package/skills/do/SKILL.md +48 -11
- package/skills/doc-sync/SKILL.md +16 -0
- package/skills/doc-sync/assets/doc_targets.md +11 -0
- package/skills/idea/SKILL.md +56 -0
- package/skills/idea/assets/idea_handoff_template.md +26 -0
- package/skills/idea/assets/idea_template.md +3 -0
- package/skills/init/SKILL.md +100 -7
- package/skills/init/assets/directory_structure.txt +1 -0
- package/skills/init/assets/workflow_template.json +1 -1
- package/skills/init/scripts/apply-project-visibility.sh +176 -0
- package/skills/init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/init/scripts/init.sh +30 -1
- package/skills/jenga/SKILL.md +160 -17
- package/skills/jenga/scripts/board-scan.sh +238 -0
- package/skills/jenga/scripts/cascade-resolve.sh +297 -0
- package/skills/jenga/scripts/render-confirmation.sh +679 -0
- package/skills/jenga/scripts/render-picker.sh +439 -0
- package/skills/jenga/scripts/resolve-id.sh +367 -0
- package/skills/jenga-permission-level/SKILL.md +81 -0
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +8 -5
- package/skills/publish/assets/ci-contract.md +2 -2
- package/skills/publish/assets/ownership-matrix.md +1 -1
- package/skills/publish/scripts/finalize_changelog.sh +115 -0
- package/skills/publish/scripts/generate_release_notes.sh +475 -28
- package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
- package/skills/publish/scripts/publish_deploy.sh +38 -8
- package/skills/publish/scripts/run_gates.sh +2 -2
- package/skills/reconcile/SKILL.md +117 -5
- package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
- package/skills/spinoff/SKILL.md +12 -7
- package/skills/todo/SKILL.md +2 -0
- package/skills/uncharted/SKILL.md +711 -0
- package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
- package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
- package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
- package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
- package/skills/uncharted/scripts/detect-tests.sh +553 -0
- package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
- package/skills/uncharted/scripts/enumerate-target.sh +470 -0
- package/skills/uncharted/scripts/import-source.sh +517 -0
- package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
- package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
- package/skills/uncharted/scripts/run-engine.sh +655 -0
- package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
- package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
- package/skills/wtf/SKILL.md +20 -0
- package/templates/CHANGELOG_TEMPLATE.md +13 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
- package/templates/permission-levels/README.md +73 -0
- package/templates/permission-levels/level-1-locked.json +71 -0
- package/templates/permission-levels/level-2-guarded.json +64 -0
- package/templates/permission-levels/level-3-standard.json +62 -0
- package/templates/permission-levels/level-4-elevated.json +60 -0
- package/templates/permission-levels/level-5-unrestricted.json +58 -0
- package/skills/convert/SKILL.md +0 -124
- package/skills/convert/convert_cli.py +0 -235
- package/skills/convert/tests/sample.csv +0 -4
- package/skills/convert/tests/sample.json +0 -5
- package/skills/convert/tests/sample.jsonl +0 -3
- package/skills/convert/tests/sample.yaml +0 -18
- package/skills/convert/tests/sample_obj.csv +0 -2
- package/skills/convert/tests/sample_obj.json +0 -9
- package/skills/mirror-public/SKILL.md +0 -237
- package/skills/mirror-public/assets/config.json +0 -5
- package/skills/mirror-public/scripts/mirror.sh +0 -374
- package/skills/self-sync/SKILL.md +0 -73
- package/skills/self-sync/scripts/run.js +0 -136
- package/skills/strategy/SKILL.md +0 -312
|
@@ -66,6 +66,83 @@ git rev-parse --verify "$TO_REF" >/dev/null 2>&1 || {
|
|
|
66
66
|
exit 1
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# .publicignore pre-filter (E36_S02_T02).
|
|
71
|
+
#
|
|
72
|
+
# A commit (or completed task) whose changed files are ALL covered by a
|
|
73
|
+
# repo-root .publicignore blocklist must never surface in CHANGELOG.md,
|
|
74
|
+
# since CHANGELOG.md itself is not blocklisted and ships to the public
|
|
75
|
+
# mirror via /mirror-public. Partial overlap is not grounds for exclusion.
|
|
76
|
+
# Absence of .publicignore is a strict no-op — PI_ACTIVE stays 0 and every
|
|
77
|
+
# _pi_* helper below fails open immediately.
|
|
78
|
+
#
|
|
79
|
+
# Matching semantics are borrowed from skills/mirror-public/scripts/mirror.sh
|
|
80
|
+
# rather than reimplemented: that script's compute_ship_list asks rsync
|
|
81
|
+
# itself "what would transfer past --exclude-from=.publicignore?" and that
|
|
82
|
+
# is the single source of truth /mirror-public --dry-run reports as
|
|
83
|
+
# shipped/blocked. This script has no real destination tree to rsync into,
|
|
84
|
+
# so _pi_classify_paths below builds a throwaway stub tree with zero-byte
|
|
85
|
+
# files at each candidate path and asks rsync the identical question.
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
PUBLICIGNORE="$PUBLISH_REPO_ROOT/.publicignore"
|
|
89
|
+
PI_ACTIVE=0
|
|
90
|
+
PI_SHIPPED_FILE=""
|
|
91
|
+
trap '[[ -n "$PI_SHIPPED_FILE" ]] && rm -f "$PI_SHIPPED_FILE"' EXIT
|
|
92
|
+
|
|
93
|
+
if [[ -f "$PUBLICIGNORE" ]]; then
|
|
94
|
+
if command -v rsync >/dev/null 2>&1; then
|
|
95
|
+
PI_ACTIVE=1
|
|
96
|
+
else
|
|
97
|
+
echo "generate_release_notes.sh: rsync not found; skipping .publicignore filtering" >&2
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# _pi_classify_paths <path-list-file> <out-shipped-file>
|
|
102
|
+
#
|
|
103
|
+
# <path-list-file> is a deduped, one-path-per-line file of repo-relative
|
|
104
|
+
# paths. Writes the subset rsync would transfer past .publicignore's
|
|
105
|
+
# --exclude-from (i.e. the NOT-blocked subset) to <out-shipped-file>. A path
|
|
106
|
+
# absent from that output is blocked.
|
|
107
|
+
_pi_classify_paths() {
|
|
108
|
+
local path_list="$1" out_file="$2"
|
|
109
|
+
local stub_src stub_dst p
|
|
110
|
+
|
|
111
|
+
stub_src="$(mktemp -d)"
|
|
112
|
+
stub_dst="$(mktemp -d)"
|
|
113
|
+
|
|
114
|
+
while IFS= read -r p || [[ -n "$p" ]]; do
|
|
115
|
+
[[ -n "$p" ]] || continue
|
|
116
|
+
mkdir -p "$stub_src/$(dirname "$p")"
|
|
117
|
+
: > "$stub_src/$p"
|
|
118
|
+
done < "$path_list"
|
|
119
|
+
|
|
120
|
+
rsync -a \
|
|
121
|
+
--exclude-from="$PUBLICIGNORE" \
|
|
122
|
+
--exclude=".git" \
|
|
123
|
+
--dry-run --itemize-changes --out-format='%i %n' \
|
|
124
|
+
"$stub_src/" "$stub_dst/" \
|
|
125
|
+
| awk '{
|
|
126
|
+
flag = $1;
|
|
127
|
+
first = substr(flag, 1, 1);
|
|
128
|
+
second = substr(flag, 2, 1);
|
|
129
|
+
if (second == "d") next;
|
|
130
|
+
keep = 0;
|
|
131
|
+
if (first == ">" || first == "<") keep = 1;
|
|
132
|
+
else if (first == "c" && second == "L") keep = 1;
|
|
133
|
+
else if (first == "h") keep = 1;
|
|
134
|
+
if (!keep) next;
|
|
135
|
+
$1 = "";
|
|
136
|
+
sub(/^ /, "");
|
|
137
|
+
sub(/\/$/, "");
|
|
138
|
+
if (length($0) > 0) print $0;
|
|
139
|
+
}' \
|
|
140
|
+
| LC_ALL=C sort -u > "$out_file"
|
|
141
|
+
|
|
142
|
+
rm -rf "$stub_src" "$stub_dst"
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
69
146
|
HISTORY_FILE="$(publish_resolve_history_file "$CONFIG_PATH")"
|
|
70
147
|
LAST_TAG="$FROM_TAG"
|
|
71
148
|
FIRST_RELEASE=0
|
|
@@ -98,8 +175,83 @@ else
|
|
|
98
175
|
LOG_RANGE=("$TO_REF")
|
|
99
176
|
fi
|
|
100
177
|
|
|
101
|
-
|
|
178
|
+
COMMIT_SUBJECTS=()
|
|
179
|
+
COMMIT_SHORT_SHAS=()
|
|
180
|
+
COMMIT_FULL_SHAS=()
|
|
181
|
+
|
|
182
|
+
while IFS=$'\t' read -r subject short_sha full_sha || [[ -n "${subject:-}${short_sha:-}${full_sha:-}" ]]; do
|
|
102
183
|
[[ -z "${subject:-}" ]] && continue
|
|
184
|
+
COMMIT_SUBJECTS+=("$subject")
|
|
185
|
+
COMMIT_SHORT_SHAS+=("$short_sha")
|
|
186
|
+
COMMIT_FULL_SHAS+=("$full_sha")
|
|
187
|
+
done < <(git log "${LOG_RANGE[@]}" --pretty=format:'%s%x09%h%x09%H')
|
|
188
|
+
|
|
189
|
+
# COMMIT_FILES[i] holds the newline-joined changed-file list for the commit
|
|
190
|
+
# at the same index as COMMIT_SUBJECTS/COMMIT_SHORT_SHAS/COMMIT_FULL_SHAS.
|
|
191
|
+
# Only populated when PI_ACTIVE, and only used to decide exclusion — it does
|
|
192
|
+
# not change classification (feat/fix/other) for anything that survives.
|
|
193
|
+
COMMIT_FILES=()
|
|
194
|
+
|
|
195
|
+
if (( PI_ACTIVE )) && (( ${#COMMIT_FULL_SHAS[@]} > 0 )); then
|
|
196
|
+
PI_ALL_PATHS_FILE="$(mktemp)"
|
|
197
|
+
: > "$PI_ALL_PATHS_FILE"
|
|
198
|
+
ci=0
|
|
199
|
+
while (( ci < ${#COMMIT_FULL_SHAS[@]} )); do
|
|
200
|
+
files="$(git diff-tree --no-commit-id --name-only -r "${COMMIT_FULL_SHAS[$ci]}" 2>/dev/null || true)"
|
|
201
|
+
COMMIT_FILES+=("$files")
|
|
202
|
+
if [[ -n "$files" ]]; then
|
|
203
|
+
printf '%s\n' "$files" >> "$PI_ALL_PATHS_FILE"
|
|
204
|
+
fi
|
|
205
|
+
ci=$((ci + 1))
|
|
206
|
+
done
|
|
207
|
+
LC_ALL=C sort -u "$PI_ALL_PATHS_FILE" -o "$PI_ALL_PATHS_FILE"
|
|
208
|
+
if [[ -s "$PI_ALL_PATHS_FILE" ]]; then
|
|
209
|
+
PI_SHIPPED_FILE="$(mktemp)"
|
|
210
|
+
_pi_classify_paths "$PI_ALL_PATHS_FILE" "$PI_SHIPPED_FILE"
|
|
211
|
+
fi
|
|
212
|
+
rm -f "$PI_ALL_PATHS_FILE"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# _pi_commit_is_fully_blocked <index>
|
|
216
|
+
#
|
|
217
|
+
# True (return 0) only when the commit at COMMIT_FILES[<index>] changed at
|
|
218
|
+
# least one file and NONE of its changed files are in the shipped set (i.e.
|
|
219
|
+
# every changed file is blocked by .publicignore). A commit with zero
|
|
220
|
+
# changed files (e.g. an empty commit) has nothing to check and fails open.
|
|
221
|
+
_pi_commit_is_fully_blocked() {
|
|
222
|
+
local idx="$1" files f
|
|
223
|
+
|
|
224
|
+
if (( ! PI_ACTIVE )); then
|
|
225
|
+
return 1
|
|
226
|
+
fi
|
|
227
|
+
if [[ -z "$PI_SHIPPED_FILE" ]]; then
|
|
228
|
+
return 1
|
|
229
|
+
fi
|
|
230
|
+
files="${COMMIT_FILES[$idx]:-}"
|
|
231
|
+
if [[ -z "$files" ]]; then
|
|
232
|
+
return 1
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
while IFS= read -r f || [[ -n "$f" ]]; do
|
|
236
|
+
[[ -n "$f" ]] || continue
|
|
237
|
+
if grep -qxF "$f" "$PI_SHIPPED_FILE" 2>/dev/null; then
|
|
238
|
+
return 1
|
|
239
|
+
fi
|
|
240
|
+
done <<< "$files"
|
|
241
|
+
|
|
242
|
+
return 0
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ci=0
|
|
246
|
+
while (( ci < ${#COMMIT_SUBJECTS[@]} )); do
|
|
247
|
+
subject="${COMMIT_SUBJECTS[$ci]}"
|
|
248
|
+
short_sha="${COMMIT_SHORT_SHAS[$ci]}"
|
|
249
|
+
|
|
250
|
+
if _pi_commit_is_fully_blocked "$ci"; then
|
|
251
|
+
ci=$((ci + 1))
|
|
252
|
+
continue
|
|
253
|
+
fi
|
|
254
|
+
|
|
103
255
|
line="- $subject ($short_sha)"
|
|
104
256
|
if printf '%s\n' "$subject" | grep -Eq '^feat(\([^)]+\))?!?: '; then
|
|
105
257
|
FEATURES+=("$line")
|
|
@@ -108,7 +260,53 @@ while IFS=$'\t' read -r subject short_sha || [[ -n "${subject:-}${short_sha:-}"
|
|
|
108
260
|
else
|
|
109
261
|
OTHER+=("$line")
|
|
110
262
|
fi
|
|
111
|
-
|
|
263
|
+
ci=$((ci + 1))
|
|
264
|
+
done
|
|
265
|
+
|
|
266
|
+
# _pi_task_is_fully_blocked <task_id>
|
|
267
|
+
#
|
|
268
|
+
# Best-effort task-to-commit association: finds commits in the resolved
|
|
269
|
+
# LOG_RANGE whose subject follows this repo's own EST commit convention
|
|
270
|
+
# (e.g. "task(E36_S02_T01): ..." — confirmed against real history), via a
|
|
271
|
+
# literal (non-regex) substring match on "($task_id)". If no associated
|
|
272
|
+
# commit is found, fails open (returns 1 — keep the entry): we only ever
|
|
273
|
+
# suppress an entry we have positive evidence for, never one we can't
|
|
274
|
+
# determine. If associated commits are found, unions their changed files
|
|
275
|
+
# and excludes the task only when every one of those files is blocked.
|
|
276
|
+
_pi_task_is_fully_blocked() {
|
|
277
|
+
local task_id="$1"
|
|
278
|
+
local shas all_paths tmp_ship sha result
|
|
279
|
+
|
|
280
|
+
result=1
|
|
281
|
+
|
|
282
|
+
if (( PI_ACTIVE )); then
|
|
283
|
+
shas="$(git log "${LOG_RANGE[@]}" --grep="($task_id)" -F --pretty=format:%H 2>/dev/null || true)"
|
|
284
|
+
if [[ -n "$shas" ]]; then
|
|
285
|
+
all_paths="$(mktemp)"
|
|
286
|
+
: > "$all_paths"
|
|
287
|
+
while IFS= read -r sha || [[ -n "$sha" ]]; do
|
|
288
|
+
[[ -n "$sha" ]] || continue
|
|
289
|
+
git diff-tree --no-commit-id --name-only -r "$sha" 2>/dev/null || true
|
|
290
|
+
done <<< "$shas" >> "$all_paths"
|
|
291
|
+
LC_ALL=C sort -u "$all_paths" -o "$all_paths"
|
|
292
|
+
|
|
293
|
+
if [[ -s "$all_paths" ]]; then
|
|
294
|
+
tmp_ship="$(mktemp)"
|
|
295
|
+
_pi_classify_paths "$all_paths" "$tmp_ship"
|
|
296
|
+
if [[ ! -s "$tmp_ship" ]]; then
|
|
297
|
+
result=0
|
|
298
|
+
fi
|
|
299
|
+
rm -f "$tmp_ship"
|
|
300
|
+
fi
|
|
301
|
+
rm -f "$all_paths"
|
|
302
|
+
fi
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
if (( result == 0 )); then
|
|
306
|
+
return 0
|
|
307
|
+
fi
|
|
308
|
+
return 1
|
|
309
|
+
}
|
|
112
310
|
|
|
113
311
|
collect_completed_tasks() {
|
|
114
312
|
local cutoff_date="$1"
|
|
@@ -137,6 +335,11 @@ collect_completed_tasks() {
|
|
|
137
335
|
task_title="$(awk '/^title: /{sub(/^title: /, ""); print; exit}' "$task_file" 2>/dev/null || true)"
|
|
138
336
|
[[ -n "$task_id" ]] || continue
|
|
139
337
|
[[ -n "$task_title" ]] || task_title="$task_id"
|
|
338
|
+
|
|
339
|
+
if (( PI_ACTIVE )) && _pi_task_is_fully_blocked "$task_id"; then
|
|
340
|
+
continue
|
|
341
|
+
fi
|
|
342
|
+
|
|
140
343
|
collected+=("- $task_id — $task_title")
|
|
141
344
|
done < <(find "$tasks_dir" -maxdepth 1 -type f -name '*.md' -print0 2>/dev/null)
|
|
142
345
|
|
|
@@ -145,8 +348,11 @@ collect_completed_tasks() {
|
|
|
145
348
|
fi
|
|
146
349
|
}
|
|
147
350
|
|
|
148
|
-
|
|
149
|
-
|
|
351
|
+
STANDALONE=0
|
|
352
|
+
if [[ -n "$OUTPUT_PATH" ]]; then
|
|
353
|
+
STANDALONE=1
|
|
354
|
+
else
|
|
355
|
+
OUTPUT_PATH="$PUBLISH_REPO_ROOT/CHANGELOG.md"
|
|
150
356
|
fi
|
|
151
357
|
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
|
152
358
|
|
|
@@ -167,34 +373,275 @@ write_section() {
|
|
|
167
373
|
} >> "$OUTPUT_PATH"
|
|
168
374
|
}
|
|
169
375
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Standing-changelog merge helpers.
|
|
378
|
+
#
|
|
379
|
+
# These operate on a plain indexed bash array (CL_LINES), loaded via a
|
|
380
|
+
# `while read` loop rather than `mapfile`/`readarray`. This repo's target
|
|
381
|
+
# shell is bash 3.2 (the stock macOS default; no Homebrew bash is assumed on
|
|
382
|
+
# PATH), which lacks `mapfile`, `declare -A`, and `local -n` namerefs
|
|
383
|
+
# (bash 4.0/4.3+ only) — none of those are used anywhere else in this repo's
|
|
384
|
+
# scripts either. Helper functions rely on bash's dynamic scoping (a
|
|
385
|
+
# function's `local`s are visible to functions it calls) instead of
|
|
386
|
+
# namerefs to share the working array and computed indices.
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
CL_LINES=()
|
|
390
|
+
CL_UNRELEASED_START=-1
|
|
391
|
+
CL_UNRELEASED_END=-1
|
|
392
|
+
CL_SUB_HEAD=-1
|
|
393
|
+
CL_SUB_END=-1
|
|
394
|
+
CL_NEW_ITEMS=()
|
|
395
|
+
CL_FILTERED=()
|
|
396
|
+
|
|
397
|
+
_cl_load() {
|
|
398
|
+
CL_LINES=()
|
|
399
|
+
local line
|
|
400
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
401
|
+
CL_LINES+=("$line")
|
|
402
|
+
done < "$1"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_cl_save() {
|
|
406
|
+
if (( ${#CL_LINES[@]} > 0 )); then
|
|
407
|
+
printf '%s\n' "${CL_LINES[@]}" > "$1"
|
|
179
408
|
else
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
409
|
+
: > "$1"
|
|
410
|
+
fi
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# Locates the `## [Unreleased]` heading and the boundary of its section (the
|
|
414
|
+
# next line starting with `## `, or EOF). Sets CL_UNRELEASED_START/_END.
|
|
415
|
+
_cl_locate_unreleased() {
|
|
416
|
+
local n=${#CL_LINES[@]}
|
|
417
|
+
local i
|
|
418
|
+
CL_UNRELEASED_START=-1
|
|
419
|
+
CL_UNRELEASED_END=$n
|
|
420
|
+
for (( i=0; i<n; i++ )); do
|
|
421
|
+
if [[ "${CL_LINES[$i]}" == "## [Unreleased]" ]]; then
|
|
422
|
+
CL_UNRELEASED_START=$i
|
|
423
|
+
break
|
|
183
424
|
fi
|
|
425
|
+
done
|
|
426
|
+
(( CL_UNRELEASED_START >= 0 )) || return 1
|
|
427
|
+
for (( i=CL_UNRELEASED_START+1; i<n; i++ )); do
|
|
428
|
+
case "${CL_LINES[$i]}" in
|
|
429
|
+
"## "*) CL_UNRELEASED_END=$i; return 0 ;;
|
|
430
|
+
esac
|
|
431
|
+
done
|
|
432
|
+
return 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Locates a `### <heading>` subsection within the current Unreleased bounds
|
|
436
|
+
# and its own boundary (next `### ` line, or the Unreleased boundary). Sets
|
|
437
|
+
# CL_SUB_HEAD (-1 if the heading doesn't exist yet) and CL_SUB_END.
|
|
438
|
+
_cl_locate_subsection() {
|
|
439
|
+
local heading="$1"
|
|
440
|
+
local j k
|
|
441
|
+
CL_SUB_HEAD=-1
|
|
442
|
+
CL_SUB_END=$CL_UNRELEASED_END
|
|
443
|
+
for (( j=CL_UNRELEASED_START+1; j<CL_UNRELEASED_END; j++ )); do
|
|
444
|
+
if [[ "${CL_LINES[$j]}" == "### $heading" ]]; then
|
|
445
|
+
CL_SUB_HEAD=$j
|
|
446
|
+
CL_SUB_END=$CL_UNRELEASED_END
|
|
447
|
+
for (( k=j+1; k<CL_UNRELEASED_END; k++ )); do
|
|
448
|
+
case "${CL_LINES[$k]}" in
|
|
449
|
+
"### "*) CL_SUB_END=$k; break ;;
|
|
450
|
+
esac
|
|
451
|
+
done
|
|
452
|
+
break
|
|
453
|
+
fi
|
|
454
|
+
done
|
|
455
|
+
return 0
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
# Filters CL_NEW_ITEMS down to entries not already present within the
|
|
459
|
+
# current CL_SUB_HEAD..CL_SUB_END bounds, into CL_FILTERED. Dedup key is the
|
|
460
|
+
# trailing "(shortsha)" token for mode=suffix (git-log-derived entries), or
|
|
461
|
+
# the "- $task_id —" prefix for mode=prefix (completed-task entries).
|
|
462
|
+
_cl_filter_new() {
|
|
463
|
+
local mode="$1"
|
|
464
|
+
CL_FILTERED=()
|
|
465
|
+
# bash 3.2 (no Homebrew bash assumed on PATH) treats a zero-element array
|
|
466
|
+
# expansion as unset under `set -u`, so every "${arr[@]}" below is guarded
|
|
467
|
+
# by an element-count check first — same convention collect_completed_tasks()
|
|
468
|
+
# already uses in this file.
|
|
469
|
+
(( ${#CL_NEW_ITEMS[@]} > 0 )) || return 0
|
|
470
|
+
if (( CL_SUB_HEAD < 0 )); then
|
|
471
|
+
CL_FILTERED=("${CL_NEW_ITEMS[@]}")
|
|
472
|
+
return 0
|
|
184
473
|
fi
|
|
185
|
-
|
|
474
|
+
local item key dup l
|
|
475
|
+
for item in "${CL_NEW_ITEMS[@]}"; do
|
|
476
|
+
if [[ "$mode" == "suffix" ]]; then
|
|
477
|
+
key="${item##* }"
|
|
478
|
+
else
|
|
479
|
+
key="${item%% — *} —"
|
|
480
|
+
fi
|
|
481
|
+
dup=0
|
|
482
|
+
for (( l=CL_SUB_HEAD+1; l<CL_SUB_END; l++ )); do
|
|
483
|
+
if [[ "${CL_LINES[$l]}" == *"$key"* ]]; then
|
|
484
|
+
dup=1
|
|
485
|
+
break
|
|
486
|
+
fi
|
|
487
|
+
done
|
|
488
|
+
(( dup )) || CL_FILTERED+=("$item")
|
|
489
|
+
done
|
|
490
|
+
return 0
|
|
491
|
+
}
|
|
186
492
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
493
|
+
# Splices the given lines into CL_LINES at index $1.
|
|
494
|
+
#
|
|
495
|
+
# NOTE: guards below use explicit `if` rather than `(( cond )) && action`.
|
|
496
|
+
# The latter is fine as a mid-function statement, but bash's `set -e`
|
|
497
|
+
# propagates a false `(( cond ))` left operand as the *function's own*
|
|
498
|
+
# return status when it is the last statement executed in the function —
|
|
499
|
+
# even though the same construct is a documented-safe no-op at the top
|
|
500
|
+
# level of a script. That silently aborted the whole script under
|
|
501
|
+
# `set -euo pipefail` the first time this was exercised end-to-end.
|
|
502
|
+
_cl_splice() {
|
|
503
|
+
local at="$1"; shift
|
|
504
|
+
local before=()
|
|
505
|
+
local after=()
|
|
506
|
+
if (( at > 0 )); then
|
|
507
|
+
before=("${CL_LINES[@]:0:at}")
|
|
508
|
+
fi
|
|
509
|
+
if (( at < ${#CL_LINES[@]} )); then
|
|
510
|
+
after=("${CL_LINES[@]:at}")
|
|
511
|
+
fi
|
|
512
|
+
CL_LINES=()
|
|
513
|
+
if (( ${#before[@]} > 0 )); then
|
|
514
|
+
CL_LINES+=("${before[@]}")
|
|
515
|
+
fi
|
|
516
|
+
if (( $# > 0 )); then
|
|
517
|
+
CL_LINES+=("$@")
|
|
518
|
+
fi
|
|
519
|
+
if (( ${#after[@]} > 0 )); then
|
|
520
|
+
CL_LINES+=("${after[@]}")
|
|
521
|
+
fi
|
|
522
|
+
return 0
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# Dedups CL_NEW_ITEMS against the named `### <heading>` subsection (creating
|
|
526
|
+
# the heading at the end of the Unreleased section if it doesn't exist yet —
|
|
527
|
+
# used for "Completed tasks", which the template doesn't pre-declare) and
|
|
528
|
+
# splices in whatever survives. Re-locates the Unreleased/subsection bounds
|
|
529
|
+
# fresh on every call, so calls can run in the subsections' natural file
|
|
530
|
+
# order (Features, Bug Fixes, Other, Completed tasks) without reasoning
|
|
531
|
+
# about earlier splices invalidating later indices.
|
|
532
|
+
_cl_process_subsection() {
|
|
533
|
+
local heading="$1" mode="$2"
|
|
534
|
+
|
|
535
|
+
_cl_locate_unreleased
|
|
536
|
+
_cl_locate_subsection "$heading"
|
|
537
|
+
_cl_filter_new "$mode"
|
|
538
|
+
|
|
539
|
+
(( ${#CL_FILTERED[@]} > 0 )) || return 0
|
|
540
|
+
|
|
541
|
+
if (( CL_SUB_HEAD >= 0 )); then
|
|
542
|
+
local anchor=$CL_SUB_HEAD
|
|
543
|
+
local m
|
|
544
|
+
for (( m=CL_SUB_HEAD+1; m<CL_SUB_END; m++ )); do
|
|
545
|
+
if [[ -n "${CL_LINES[$m]}" ]]; then
|
|
546
|
+
anchor=$m
|
|
547
|
+
fi
|
|
548
|
+
done
|
|
549
|
+
_cl_splice "$((anchor + 1))" "${CL_FILTERED[@]}"
|
|
550
|
+
else
|
|
551
|
+
local insert_at=$CL_UNRELEASED_END
|
|
552
|
+
local new_block=()
|
|
553
|
+
if (( insert_at > CL_UNRELEASED_START + 1 )) && [[ -n "${CL_LINES[$((insert_at - 1))]}" ]]; then
|
|
554
|
+
new_block+=("")
|
|
555
|
+
fi
|
|
556
|
+
new_block+=("### $heading" "" "${CL_FILTERED[@]}")
|
|
557
|
+
if (( insert_at < ${#CL_LINES[@]} )); then
|
|
558
|
+
new_block+=("")
|
|
559
|
+
fi
|
|
560
|
+
_cl_splice "$insert_at" "${new_block[@]}"
|
|
561
|
+
fi
|
|
562
|
+
return 0
|
|
563
|
+
}
|
|
190
564
|
|
|
191
|
-
|
|
192
|
-
|
|
565
|
+
if (( STANDALONE )); then
|
|
566
|
+
GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
193
567
|
{
|
|
194
|
-
printf '
|
|
195
|
-
printf '%s\n' "$
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
fi
|
|
568
|
+
printf '# Release notes\n\n'
|
|
569
|
+
printf -- '- Generated: %s\n' "$GENERATED_AT"
|
|
570
|
+
if [[ -n "$TARGET_NAME" ]]; then
|
|
571
|
+
printf -- '- Target: %s\n' "$TARGET_NAME"
|
|
572
|
+
fi
|
|
573
|
+
if [[ -n "$LAST_TAG" ]]; then
|
|
574
|
+
printf -- '- Changes: %s..%s\n\n' "$LAST_TAG" "$TO_REF"
|
|
575
|
+
else
|
|
576
|
+
printf -- '- Changes: full history through %s\n\n' "$TO_REF"
|
|
577
|
+
if (( FIRST_RELEASE )); then
|
|
578
|
+
printf '> First release — full history included\n\n'
|
|
579
|
+
fi
|
|
580
|
+
fi
|
|
581
|
+
} > "$OUTPUT_PATH"
|
|
582
|
+
|
|
583
|
+
write_section 'Features' FEATURES
|
|
584
|
+
write_section 'Bug Fixes' BUG_FIXES
|
|
585
|
+
write_section 'Other' OTHER
|
|
586
|
+
|
|
587
|
+
COMPLETED_TASKS="$(collect_completed_tasks "$CUTOFF_DATE" || true)"
|
|
588
|
+
if [[ -n "$COMPLETED_TASKS" ]]; then
|
|
589
|
+
{
|
|
590
|
+
printf '### Completed tasks\n'
|
|
591
|
+
printf '%s\n' "$COMPLETED_TASKS"
|
|
592
|
+
printf '\n'
|
|
593
|
+
} >> "$OUTPUT_PATH"
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
printf '📝 Release notes draft written to %s\n' "$OUTPUT_PATH"
|
|
597
|
+
else
|
|
598
|
+
CHANGELOG_TEMPLATE="$PUBLISH_REPO_ROOT/templates/CHANGELOG_TEMPLATE.md"
|
|
599
|
+
if [[ ! -f "$OUTPUT_PATH" ]]; then
|
|
600
|
+
[[ -f "$CHANGELOG_TEMPLATE" ]] || {
|
|
601
|
+
echo "Changelog template not found: $CHANGELOG_TEMPLATE" >&2
|
|
602
|
+
exit 1
|
|
603
|
+
}
|
|
604
|
+
cp "$CHANGELOG_TEMPLATE" "$OUTPUT_PATH"
|
|
605
|
+
fi
|
|
199
606
|
|
|
200
|
-
|
|
607
|
+
_cl_load "$OUTPUT_PATH"
|
|
608
|
+
_cl_locate_unreleased || {
|
|
609
|
+
echo "generate_release_notes.sh: '## [Unreleased]' section not found in $OUTPUT_PATH — cannot merge entries." >&2
|
|
610
|
+
exit 1
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
CL_NEW_ITEMS=()
|
|
614
|
+
if (( ${#FEATURES[@]} > 0 )); then
|
|
615
|
+
CL_NEW_ITEMS=("${FEATURES[@]}")
|
|
616
|
+
fi
|
|
617
|
+
_cl_process_subsection 'Features' suffix
|
|
618
|
+
|
|
619
|
+
CL_NEW_ITEMS=()
|
|
620
|
+
if (( ${#BUG_FIXES[@]} > 0 )); then
|
|
621
|
+
CL_NEW_ITEMS=("${BUG_FIXES[@]}")
|
|
622
|
+
fi
|
|
623
|
+
_cl_process_subsection 'Bug Fixes' suffix
|
|
624
|
+
|
|
625
|
+
CL_NEW_ITEMS=()
|
|
626
|
+
if (( ${#OTHER[@]} > 0 )); then
|
|
627
|
+
CL_NEW_ITEMS=("${OTHER[@]}")
|
|
628
|
+
fi
|
|
629
|
+
_cl_process_subsection 'Other' suffix
|
|
630
|
+
|
|
631
|
+
COMPLETED_TASKS="$(collect_completed_tasks "$CUTOFF_DATE" || true)"
|
|
632
|
+
CL_NEW_ITEMS=()
|
|
633
|
+
if [[ -n "$COMPLETED_TASKS" ]]; then
|
|
634
|
+
OLD_IFS="$IFS"
|
|
635
|
+
IFS=$'\n'
|
|
636
|
+
# Intentional word-split on newline (no mapfile in bash 3.2); each
|
|
637
|
+
# collect_completed_tasks() line is a single "- ID — title" entry.
|
|
638
|
+
# shellcheck disable=SC2206
|
|
639
|
+
CL_NEW_ITEMS=($COMPLETED_TASKS)
|
|
640
|
+
IFS="$OLD_IFS"
|
|
641
|
+
fi
|
|
642
|
+
_cl_process_subsection 'Completed tasks' prefix
|
|
643
|
+
|
|
644
|
+
_cl_save "$OUTPUT_PATH"
|
|
645
|
+
|
|
646
|
+
printf '📝 CHANGELOG.md updated: %s\n' "$OUTPUT_PATH"
|
|
647
|
+
fi
|
|
@@ -38,10 +38,11 @@ source "${SCRIPT_DIR}/publish_common.sh"
|
|
|
38
38
|
TARGET_NAME=""
|
|
39
39
|
CONFIG_PATH=""
|
|
40
40
|
DRY_RUN=0
|
|
41
|
+
VERSION=""
|
|
41
42
|
|
|
42
43
|
usage() {
|
|
43
44
|
cat <<'USAGE'
|
|
44
|
-
Usage: npm_ci_pipeline.sh --target <name> --config <path> [--dry-run]
|
|
45
|
+
Usage: npm_ci_pipeline.sh --target <name> --config <path> [--version <vX.Y.Z>] [--dry-run]
|
|
45
46
|
USAGE
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -75,6 +76,18 @@ while [[ $# -gt 0 ]]; do
|
|
|
75
76
|
DRY_RUN=1
|
|
76
77
|
shift
|
|
77
78
|
;;
|
|
79
|
+
--version)
|
|
80
|
+
[[ $# -ge 2 ]] || fail_usage "--version requires a value"
|
|
81
|
+
VERSION="$2"
|
|
82
|
+
shift 2
|
|
83
|
+
;;
|
|
84
|
+
--version=*)
|
|
85
|
+
VERSION="${1#*=}"
|
|
86
|
+
shift
|
|
87
|
+
;;
|
|
88
|
+
--non-interactive)
|
|
89
|
+
shift
|
|
90
|
+
;;
|
|
78
91
|
-h|--help)
|
|
79
92
|
usage
|
|
80
93
|
exit 0
|
|
@@ -182,6 +195,17 @@ if (( DRY_RUN )); then
|
|
|
182
195
|
exit 0
|
|
183
196
|
fi
|
|
184
197
|
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Phase: bump-version
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
if [[ -n "${VERSION}" ]]; then
|
|
202
|
+
# Strip leading 'v' — npm version expects bare semver (e.g. 1.0.1, not v1.0.1)
|
|
203
|
+
NPM_VERSION="${VERSION#v}"
|
|
204
|
+
printf '[bump-version] setting package.json version to %s...\n' "${NPM_VERSION}"
|
|
205
|
+
npm --prefix "${REPO_ROOT}" version "${NPM_VERSION}" --no-git-tag-version --allow-same-version
|
|
206
|
+
printf '[bump-version] done\n'
|
|
207
|
+
fi
|
|
208
|
+
|
|
185
209
|
# ---------------------------------------------------------------------------
|
|
186
210
|
# Phase: commit-workflow
|
|
187
211
|
# ---------------------------------------------------------------------------
|
|
@@ -190,16 +214,30 @@ printf '[commit-workflow] writing workflow file...\n'
|
|
|
190
214
|
mkdir -p "${WORKFLOW_DIR}"
|
|
191
215
|
printf '%s\n' "${WORKFLOW_YAML}" > "${WORKFLOW_FILE}"
|
|
192
216
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
217
|
+
CHANGED=0
|
|
218
|
+
git -C "${REPO_ROOT}" diff --quiet -- "${WORKFLOW_FILE_REL}" 2>/dev/null || CHANGED=1
|
|
219
|
+
git -C "${REPO_ROOT}" diff --quiet -- package.json package-lock.json 2>/dev/null || CHANGED=1
|
|
220
|
+
if git -C "${REPO_ROOT}" ls-files --error-unmatch "${WORKFLOW_FILE_REL}" >/dev/null 2>&1 && [[ $CHANGED -eq 0 ]]; then
|
|
196
221
|
printf '[commit-workflow] no changes — skipping commit\n'
|
|
197
222
|
else
|
|
198
|
-
git -C "${REPO_ROOT}" add "${WORKFLOW_FILE_REL}"
|
|
199
|
-
|
|
223
|
+
git -C "${REPO_ROOT}" add "${WORKFLOW_FILE_REL}" package.json
|
|
224
|
+
[[ -f "${REPO_ROOT}/package-lock.json" ]] && git -C "${REPO_ROOT}" add package-lock.json
|
|
225
|
+
git -C "${REPO_ROOT}" commit -m "chore(publish): bump to ${VERSION:-unknown} and update npm CI workflow"
|
|
200
226
|
printf '[commit-workflow] committed: %s\n' "${WORKFLOW_FILE}"
|
|
201
227
|
fi
|
|
202
228
|
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# Phase: push-workflow
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
printf '[push-workflow] pushing workflow commit to origin...\n'
|
|
233
|
+
|
|
234
|
+
if ! git -C "${REPO_ROOT}" push origin HEAD; then
|
|
235
|
+
printf '[push-workflow] ERROR: git push failed\n' >&2
|
|
236
|
+
exit "${EXIT_TRIGGER_FAILED}"
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
printf '[push-workflow] pushed successfully\n'
|
|
240
|
+
|
|
203
241
|
# ---------------------------------------------------------------------------
|
|
204
242
|
# Phase: trigger
|
|
205
243
|
# ---------------------------------------------------------------------------
|