@jenga-ai/agent 3.1.1 → 3.4.0
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 +52 -12
- package/agents/developer.md +31 -16
- package/agents/scrum-master.md +18 -17
- package/agents/tester.md +25 -15
- package/bin/jenga.js +10 -0
- package/lib/commands/dashboard.js +92 -0
- package/lib/skill-allow-list.json +7 -2
- package/package.json +21 -2
- package/project/app/api/lib/resolve-project-root.js +120 -0
- package/project/app/api/package.json +16 -0
- package/project/app/api/parsers/architecture.js +72 -0
- package/project/app/api/parsers/board.js +141 -0
- package/project/app/api/parsers/documentation.js +125 -0
- package/project/app/api/parsers/git-log.js +52 -0
- package/project/app/api/parsers/ideas.js +62 -0
- package/project/app/api/parsers/knowledge-graph.js +73 -0
- package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
- package/project/app/api/parsers/rapports.js +148 -0
- package/project/app/api/parsers/todo.js +179 -0
- package/project/app/api/response.js +47 -0
- package/project/app/api/routes/architecture.js +23 -0
- package/project/app/api/routes/board.js +46 -0
- package/project/app/api/routes/documentation.js +24 -0
- package/project/app/api/routes/health.js +25 -0
- package/project/app/api/routes/history.js +55 -0
- package/project/app/api/routes/rapports.js +24 -0
- package/project/app/api/scripts/capture-snapshot.js +294 -0
- package/project/app/api/server.js +112 -0
- package/project/app/api/types.js +40 -0
- package/project/app/package.json +21 -0
- package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
- package/project/app/ui/dist/index.html +13 -0
- package/project/app/ui/package.json +23 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
- package/project/app/ui/scripts/dashboard-open.cjs +88 -0
- package/project/app/ui/scripts/dashboard-start.cjs +87 -0
- package/scripts/acquire-concurrency-slot.sh +220 -0
- package/scripts/audit-twin-divergence.sh +625 -0
- package/scripts/check-public-playbook-steps.sh +136 -0
- package/scripts/compute-deploy-reconcile.sh +439 -0
- package/scripts/jenga-permission-level-switch.sh +19 -3
- package/scripts/mark-deployed.sh +532 -0
- package/scripts/populate-knowledge-graph.js +429 -0
- package/scripts/release-concurrency-slot.sh +129 -0
- package/scripts/validate-board.sh +60 -2
- package/scripts/verify-consumer-install.sh +470 -0
- package/skills/j-close-story/SKILL.md +1 -1
- package/skills/j-cloud-connect/SKILL.md +95 -0
- package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
- package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
- package/skills/j-dashboard/SKILL.md +144 -0
- package/skills/j-dashboard/scripts/launch.sh +121 -0
- package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
- package/skills/j-dashboard/scripts/snapshot.sh +267 -0
- package/skills/j-dashboard-share/SKILL.md +96 -0
- package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
- package/skills/j-do/SKILL.md +19 -19
- package/skills/j-doc-sync/SKILL.md +12 -1
- package/skills/j-idea/SKILL.md +1 -1
- package/skills/j-init/SKILL.md +5 -4
- package/skills/j-init/assets/directory_structure.txt +1 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +2 -2
- package/skills/j-init/scripts/init.sh +13 -2
- package/skills/j-playbook/SKILL.md +93 -0
- package/skills/j-playbook-new/SKILL.md +155 -0
- package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
- package/skills/j-proceed/SKILL.md +1 -1
- package/skills/j-publish/SKILL.md +1 -1
- package/skills/j-publish/adapters/npm-ci.md +29 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +9 -0
- package/skills/j-publish/scripts/npm_pipeline.sh +18 -0
- package/skills/j-publish/scripts/npm_stage_pipeline.sh +81 -41
- package/skills/j-reconcile/SKILL.md +1 -0
- package/skills/j-redo/SKILL.md +1 -1
- package/skills/j-status/SKILL.md +12 -0
- package/skills/j-todo/SKILL.md +2 -2
- package/skills/j-uncharted/SKILL.md +8 -7
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +18 -2
- package/skills/jenga/SKILL.md +55 -16
- package/skills/jenga/playbooks/idea-to-committed.json +20 -0
- package/skills/jenga/playbooks/schema.json +1 -1
- package/skills/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +968 -41
- package/skills/jenga/scripts/match-playbook.sh +1 -1
- package/skills/jenga/scripts/render-playbook-confirmation.sh +162 -8
- package/skills/jenga/scripts/run-playbook-step.sh +535 -42
- package/skills/jenga-permission-level/SKILL.md +4 -4
- package/templates/KNOWLEDGE_GRAPH_STUB_SCHEMA_TEMPLATE.md +128 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
- package/templates/playbook-types.json +8 -0
- package/skills/jenga/playbooks/brainstorm-to-mirror.json +0 -22
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# scripts/mark-deployed.sh
|
|
4
|
+
#
|
|
5
|
+
# Given E51_S05_T01's compute-deploy-reconcile.sh TSV output (one line per
|
|
6
|
+
# resolved, not-yet-reconciled public tag:
|
|
7
|
+
# <tag_type>\t<version>\t<public_tag_name>\t<resolved_private_sha>
|
|
8
|
+
# where tag_type is "stage" or "prod"), this script:
|
|
9
|
+
# 1. Enumerates board tickets (tasks + stories) currently at
|
|
10
|
+
# status: Publicized or status: Deployed to Stage -- never Pending,
|
|
11
|
+
# Done, Passed, etc. directly (per E51's own DoD: "only Publicized
|
|
12
|
+
# tickets are eligible for Deployed to Stage -> Deployed to Prod").
|
|
13
|
+
# 2. Derives each eligible ticket's matched EST-tagged commit SHAs (and,
|
|
14
|
+
# for parity/logging, their unioned touched-file list) using the
|
|
15
|
+
# identical anchored-grep + union technique
|
|
16
|
+
# skills/self-sync/scripts/mark-merged.sh already implements.
|
|
17
|
+
# 3. Checks ANCESTRY, not diff-membership: a ticket is "covered" by a tag
|
|
18
|
+
# only when EVERY one of its matched commits is an ancestor of that
|
|
19
|
+
# tag's resolved_private_sha (git merge-base --is-ancestor). A tag
|
|
20
|
+
# represents a cumulative release, so "has this ticket's work already
|
|
21
|
+
# landed by the time this commit was tagged" is the correct question --
|
|
22
|
+
# unlike Merged/Publicized, which check "did this specific sync/mirror
|
|
23
|
+
# run's diff include these files".
|
|
24
|
+
# 4. Writes status via scripts/with-lock.sh + close-story's existing
|
|
25
|
+
# update-task-frontmatter.sh (reused, not reimplemented):
|
|
26
|
+
# stage tag: Publicized -> Deployed to Stage. Already at
|
|
27
|
+
# Deployed to Stage or Deployed to Prod -> unchanged
|
|
28
|
+
# (idempotent, never demotes Prod back to Stage).
|
|
29
|
+
# prod tag: Publicized OR Deployed to Stage -> Deployed to Prod
|
|
30
|
+
# directly (never left at Stage alongside a separate Prod
|
|
31
|
+
# entry). Already at Deployed to Prod -> unchanged.
|
|
32
|
+
# When (and only when) a ticket is set to Deployed to Prod, this script
|
|
33
|
+
# ALSO writes date_deployed_prod: <today, ISO 8601> to the same
|
|
34
|
+
# ticket's frontmatter, inside the SAME locked write window -- one
|
|
35
|
+
# with-lock.sh call wrapping a `bash -c '... status "Deployed to
|
|
36
|
+
# Prod" && ... date_deployed_prod <today>'`, never two separately
|
|
37
|
+
# locked writes.
|
|
38
|
+
# 5. Processing order: all stage-type tag lines first, then all prod-type
|
|
39
|
+
# tag lines -- so a same-run prod tag can promote a ticket the stage
|
|
40
|
+
# pass in this same run just touched. Ticket status is re-read fresh
|
|
41
|
+
# for every tag line processed (never cached across tag iterations),
|
|
42
|
+
# since an earlier tag in the same run may have already changed it.
|
|
43
|
+
# 6. Partial or zero coverage -> ticket left unchanged, no error. A ticket
|
|
44
|
+
# with zero matched commits is left unchanged (nothing to compare).
|
|
45
|
+
# 7. Only after this run's tag lines have each been attempted, advances
|
|
46
|
+
# project/data/deploy-reconcile-marker.json's reconciled_tags array --
|
|
47
|
+
# but PER-TAG, not all-or-nothing: a tag is appended only if every
|
|
48
|
+
# board write attempted while processing it succeeded. A tag that hit
|
|
49
|
+
# a write failure (e.g. a with-lock.sh timeout on one ticket's board
|
|
50
|
+
# file) is NOT appended, so a future run retries exactly that tag,
|
|
51
|
+
# while OTHER tags in the same run that fully succeeded are still
|
|
52
|
+
# recorded. This is a deliberate, finer-grained deviation from
|
|
53
|
+
# mark-merged.sh's all-or-nothing marker advancement, per this task's
|
|
54
|
+
# own spec (a single run may resolve several tags at once, and one
|
|
55
|
+
# tag's failure should not force every sibling tag to be redone).
|
|
56
|
+
#
|
|
57
|
+
# Marker file: project/data/deploy-reconcile-marker.json. Does not exist on
|
|
58
|
+
# disk yet as of this script's authoring -- created (seeded
|
|
59
|
+
# {"reconciled_tags": []}) on first successful append, never on a
|
|
60
|
+
# nothing-to-append run. Read-side of this file is compute-deploy-
|
|
61
|
+
# reconcile.sh's job (already implemented, read-only, lock-protected); this
|
|
62
|
+
# script owns the write side exclusively.
|
|
63
|
+
#
|
|
64
|
+
# Input source (documented interface -- caller's choice of any one of),
|
|
65
|
+
# mirroring mark-merged.sh's own --diff-file / --stdin / default convention:
|
|
66
|
+
# --diff-file <path> Read TSV lines from a file (primarily for
|
|
67
|
+
# testability -- feed a synthetic input without a
|
|
68
|
+
# real compute-deploy-reconcile.sh run).
|
|
69
|
+
# --stdin Explicitly read TSV lines from stdin, e.g.
|
|
70
|
+
# `compute-deploy-reconcile.sh | mark-deployed.sh
|
|
71
|
+
# --stdin`. Not auto-detected via a `[ ! -t 0 ]`
|
|
72
|
+
# check -- see mark-merged.sh's header for why a TTY
|
|
73
|
+
# check is unsafe for a subprocess-spawned caller.
|
|
74
|
+
# (neither) Default: invoke the sibling
|
|
75
|
+
# compute-deploy-reconcile.sh directly and use its
|
|
76
|
+
# stdout.
|
|
77
|
+
#
|
|
78
|
+
# Marker-file JSON read/write uses python3 directly (no jq path) --
|
|
79
|
+
# consistent with this repo's own scripts/validate-board.sh, which requires
|
|
80
|
+
# python3 unconditionally with no jq fallback at all. The frontmatter status
|
|
81
|
+
# writes below reuse update-task-frontmatter.sh (pure bash, no interpreter
|
|
82
|
+
# dependency), matching mark-merged.sh.
|
|
83
|
+
#
|
|
84
|
+
# Exit codes:
|
|
85
|
+
# 0 success -- every attempted board write succeeded (including the
|
|
86
|
+
# "nothing to reconcile" / empty-input case)
|
|
87
|
+
# 1 one or more board writes failed during this run -- the tags NOT
|
|
88
|
+
# affected by a failure were still recorded as reconciled; only the
|
|
89
|
+
# failing tag(s) are left for a future retry. Not a hard abort: this
|
|
90
|
+
# script always finishes processing every tag line in its input before
|
|
91
|
+
# exiting.
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
set -uo pipefail
|
|
95
|
+
IFS=$'\n\t'
|
|
96
|
+
|
|
97
|
+
# -----------------------------------------------------------------------------
|
|
98
|
+
# Helpers
|
|
99
|
+
# -----------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
die() {
|
|
102
|
+
printf 'mark-deployed.sh: error: %s\n' "$*" >&2
|
|
103
|
+
exit 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
log() {
|
|
107
|
+
printf 'mark-deployed.sh: %s\n' "$*" >&2
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
warn() {
|
|
111
|
+
printf 'mark-deployed.sh: WARNING: %s\n' "$*" >&2
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
usage() {
|
|
115
|
+
cat <<'EOF'
|
|
116
|
+
Usage: mark-deployed.sh [--diff-file <path> | --stdin] [-h|--help]
|
|
117
|
+
|
|
118
|
+
Reads TSV lines (tag_type<TAB>version<TAB>public_tag_name<TAB>resolved_private_sha)
|
|
119
|
+
as produced by scripts/compute-deploy-reconcile.sh, matches eligible board
|
|
120
|
+
tickets (status: Publicized or Deployed to Stage) by commit ancestry, writes
|
|
121
|
+
Deployed to Stage / Deployed to Prod status (plus date_deployed_prod on a
|
|
122
|
+
Prod write), and advances project/data/deploy-reconcile-marker.json's
|
|
123
|
+
reconciled_tags array for each tag line that completed with zero write
|
|
124
|
+
failures.
|
|
125
|
+
|
|
126
|
+
Input source (pick one; documented in the script header):
|
|
127
|
+
--diff-file <path> Read TSV lines from a file.
|
|
128
|
+
--stdin Explicitly read TSV lines from stdin (e.g.
|
|
129
|
+
`compute-deploy-reconcile.sh | mark-deployed.sh
|
|
130
|
+
--stdin`). Not auto-detected.
|
|
131
|
+
(neither) Default: invoke the sibling
|
|
132
|
+
compute-deploy-reconcile.sh.
|
|
133
|
+
EOF
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
INPUT_FILE_ARG=""
|
|
137
|
+
READ_STDIN=0
|
|
138
|
+
|
|
139
|
+
while [ $# -gt 0 ]; do
|
|
140
|
+
case "$1" in
|
|
141
|
+
--diff-file)
|
|
142
|
+
[ $# -ge 2 ] || die "--diff-file requires a path argument"
|
|
143
|
+
INPUT_FILE_ARG="$2"
|
|
144
|
+
shift 2
|
|
145
|
+
;;
|
|
146
|
+
--stdin)
|
|
147
|
+
READ_STDIN=1
|
|
148
|
+
shift
|
|
149
|
+
;;
|
|
150
|
+
-h|--help)
|
|
151
|
+
usage
|
|
152
|
+
exit 0
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
usage >&2
|
|
156
|
+
die "unknown argument: $1"
|
|
157
|
+
;;
|
|
158
|
+
esac
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
if [ -n "$INPUT_FILE_ARG" ] && [ "$READ_STDIN" -eq 1 ]; then
|
|
162
|
+
usage >&2
|
|
163
|
+
die "--diff-file and --stdin are mutually exclusive"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# -----------------------------------------------------------------------------
|
|
167
|
+
# Locate script + repo root (same symlink-resolution + repo-root derivation
|
|
168
|
+
# pattern as mark-merged.sh / compute-deploy-reconcile.sh).
|
|
169
|
+
# -----------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
|
172
|
+
while [ -h "$SCRIPT_PATH" ]; do
|
|
173
|
+
LINK_TARGET="$(readlink "$SCRIPT_PATH")"
|
|
174
|
+
case "$LINK_TARGET" in
|
|
175
|
+
/*) SCRIPT_PATH="$LINK_TARGET" ;;
|
|
176
|
+
*) SCRIPT_PATH="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)/$LINK_TARGET" ;;
|
|
177
|
+
esac
|
|
178
|
+
done
|
|
179
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
180
|
+
|
|
181
|
+
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
182
|
+
[ -n "$REPO_ROOT" ] || die "could not locate repo root (git rev-parse failed from $SCRIPT_DIR)"
|
|
183
|
+
|
|
184
|
+
COMPUTE_SCRIPT="$SCRIPT_DIR/compute-deploy-reconcile.sh"
|
|
185
|
+
WITH_LOCK_SCRIPT="$REPO_ROOT/scripts/with-lock.sh"
|
|
186
|
+
UPDATE_FRONTMATTER_SCRIPT="$REPO_ROOT/skills/close-story/scripts/update-task-frontmatter.sh"
|
|
187
|
+
MARKER_FILE="$REPO_ROOT/project/data/deploy-reconcile-marker.json"
|
|
188
|
+
|
|
189
|
+
[ -f "$WITH_LOCK_SCRIPT" ] || die "expected script not found: $WITH_LOCK_SCRIPT"
|
|
190
|
+
[ -f "$UPDATE_FRONTMATTER_SCRIPT" ] || die "expected script not found: $UPDATE_FRONTMATTER_SCRIPT"
|
|
191
|
+
command -v python3 >/dev/null 2>&1 || die "python3 not installed (required to read/write $MARKER_FILE)"
|
|
192
|
+
|
|
193
|
+
TASKS_DIR="$REPO_ROOT/project/board/tasks"
|
|
194
|
+
STORIES_DIR="$REPO_ROOT/project/board/stories"
|
|
195
|
+
|
|
196
|
+
# -----------------------------------------------------------------------------
|
|
197
|
+
# Resolve the input source into a materialised temp file.
|
|
198
|
+
# -----------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
INPUT_FILE="$(mktemp)"
|
|
201
|
+
CLEANUP_FILES=("$INPUT_FILE")
|
|
202
|
+
cleanup() {
|
|
203
|
+
rm -f "${CLEANUP_FILES[@]}" 2>/dev/null || true
|
|
204
|
+
}
|
|
205
|
+
trap cleanup EXIT
|
|
206
|
+
|
|
207
|
+
if [ -n "$INPUT_FILE_ARG" ]; then
|
|
208
|
+
[ -f "$INPUT_FILE_ARG" ] || die "--diff-file path not found: $INPUT_FILE_ARG"
|
|
209
|
+
log "reading tag input from --diff-file: $INPUT_FILE_ARG"
|
|
210
|
+
cp "$INPUT_FILE_ARG" "$INPUT_FILE"
|
|
211
|
+
elif [ "$READ_STDIN" -eq 1 ]; then
|
|
212
|
+
log "reading tag input from stdin (--stdin)"
|
|
213
|
+
cat > "$INPUT_FILE"
|
|
214
|
+
else
|
|
215
|
+
[ -f "$COMPUTE_SCRIPT" ] || die "expected sibling script not found: $COMPUTE_SCRIPT"
|
|
216
|
+
log "no --diff-file and no --stdin -- invoking compute-deploy-reconcile.sh"
|
|
217
|
+
if ! "$COMPUTE_SCRIPT" > "$INPUT_FILE"; then
|
|
218
|
+
die "compute-deploy-reconcile.sh failed"
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# Strip accidental blank lines (same rationale as mark-merged.sh: a
|
|
223
|
+
# genuinely-empty input must compare as zero-byte, not one blank line).
|
|
224
|
+
BLANK_STRIPPED="$(mktemp)"
|
|
225
|
+
CLEANUP_FILES+=("$BLANK_STRIPPED")
|
|
226
|
+
grep -v '^[[:space:]]*$' "$INPUT_FILE" > "$BLANK_STRIPPED" 2>/dev/null || true
|
|
227
|
+
mv "$BLANK_STRIPPED" "$INPUT_FILE"
|
|
228
|
+
|
|
229
|
+
LINE_COUNT="$(wc -l < "$INPUT_FILE" | tr -d '[:space:]')"
|
|
230
|
+
log "tag input contains $LINE_COUNT line(s)"
|
|
231
|
+
|
|
232
|
+
if [ "$LINE_COUNT" -eq 0 ]; then
|
|
233
|
+
log "nothing to reconcile -- zero tag lines, zero board writes, marker not touched"
|
|
234
|
+
exit 0
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# -----------------------------------------------------------------------------
|
|
238
|
+
# Parse TSV lines into stage/prod buckets, preserving input order within
|
|
239
|
+
# each bucket.
|
|
240
|
+
# -----------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
STAGE_LINES=()
|
|
243
|
+
PROD_LINES=()
|
|
244
|
+
|
|
245
|
+
while IFS=$'\t' read -r tag_type version tag_name sha; do
|
|
246
|
+
[ -n "$tag_type" ] || continue
|
|
247
|
+
case "$tag_type" in
|
|
248
|
+
stage) STAGE_LINES+=("$tag_type"$'\t'"$version"$'\t'"$tag_name"$'\t'"$sha") ;;
|
|
249
|
+
prod) PROD_LINES+=("$tag_type"$'\t'"$version"$'\t'"$tag_name"$'\t'"$sha") ;;
|
|
250
|
+
*) warn "unrecognized tag_type '$tag_type' on input line for tag '$tag_name' -- skipping this line entirely" ;;
|
|
251
|
+
esac
|
|
252
|
+
done < "$INPUT_FILE"
|
|
253
|
+
|
|
254
|
+
log "parsed ${#STAGE_LINES[@]} stage line(s), ${#PROD_LINES[@]} prod line(s)"
|
|
255
|
+
|
|
256
|
+
# -----------------------------------------------------------------------------
|
|
257
|
+
# Frontmatter field reader + trim (identical to mark-merged.sh's own
|
|
258
|
+
# helpers -- small, tolerated duplication rather than a third shared-helper
|
|
259
|
+
# script for two ~10-line functions; this repo already tolerates the same
|
|
260
|
+
# duplication between mark-merged.sh and compute-deploy-reconcile.sh's own
|
|
261
|
+
# COPY_SET extraction).
|
|
262
|
+
# -----------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
read_frontmatter_field() {
|
|
265
|
+
local file="$1" key="$2"
|
|
266
|
+
awk -v key="$key" '
|
|
267
|
+
NR==1 && $0=="---" { infm=1; next }
|
|
268
|
+
infm && $0=="---" { exit }
|
|
269
|
+
infm && $0 ~ ("^" key ":") {
|
|
270
|
+
sub("^" key ":[ \t]*", "", $0)
|
|
271
|
+
print $0
|
|
272
|
+
exit
|
|
273
|
+
}
|
|
274
|
+
' "$file"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
trim() {
|
|
278
|
+
local s="$1"
|
|
279
|
+
s="${s#"${s%%[![:space:]]*}"}"
|
|
280
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
281
|
+
s="${s%\"}"; s="${s#\"}"
|
|
282
|
+
s="${s%\'}"; s="${s#\'}"
|
|
283
|
+
printf '%s' "$s"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# -----------------------------------------------------------------------------
|
|
287
|
+
# Commit-matching + ancestry check
|
|
288
|
+
# -----------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
# Prints newline-separated matched EST-tagged commit SHAs for a ticket id
|
|
291
|
+
# (anchored grep, identical to mark-merged.sh -- guards against a story id
|
|
292
|
+
# being a literal string prefix of its own tasks' ids). Empty output means
|
|
293
|
+
# zero matched commits.
|
|
294
|
+
commits_for_ticket() {
|
|
295
|
+
local id="$1"
|
|
296
|
+
git -C "$REPO_ROOT" log --all --no-merges --extended-regexp \
|
|
297
|
+
--grep="${id}([^0-9_]|\$)" --pretty=format:'%H' 2>/dev/null || true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
commit_files() {
|
|
301
|
+
local sha="$1"
|
|
302
|
+
if git -C "$REPO_ROOT" rev-parse --verify -q "${sha}~1" >/dev/null 2>&1; then
|
|
303
|
+
git -C "$REPO_ROOT" diff --name-only "${sha}~1..${sha}" 2>/dev/null || true
|
|
304
|
+
else
|
|
305
|
+
git -C "$REPO_ROOT" diff-tree --no-commit-id --name-only -r "$sha" 2>/dev/null || true
|
|
306
|
+
fi
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Sorted-unique union of every file touched by every commit matching this
|
|
310
|
+
# ticket id. Kept for parity/logging with mark-merged.sh's derivation, even
|
|
311
|
+
# though the actual coverage decision below is ancestry-based, not
|
|
312
|
+
# diff-membership-based.
|
|
313
|
+
collect_touched_files() {
|
|
314
|
+
local shas="$1" sha
|
|
315
|
+
[ -n "$shas" ] || return 0
|
|
316
|
+
while IFS= read -r sha; do
|
|
317
|
+
[ -n "$sha" ] || continue
|
|
318
|
+
commit_files "$sha"
|
|
319
|
+
done <<< "$shas" | sort -u
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# True (0) iff EVERY commit sha in $1 (newline-separated, non-empty) is an
|
|
323
|
+
# ancestor of $2 (the tag's resolved private SHA). A commit sha that cannot
|
|
324
|
+
# be resolved to an ancestry relationship (e.g. object graph oddity) counts
|
|
325
|
+
# as "not an ancestor" rather than a hard script error -- coverage simply
|
|
326
|
+
# fails closed for that ticket/tag pair, and the ticket is left unchanged.
|
|
327
|
+
all_ancestors_of() {
|
|
328
|
+
local shas="$1" target_sha="$2" sha
|
|
329
|
+
while IFS= read -r sha; do
|
|
330
|
+
[ -n "$sha" ] || continue
|
|
331
|
+
git -C "$REPO_ROOT" merge-base --is-ancestor "$sha" "$target_sha" 2>/dev/null || return 1
|
|
332
|
+
done <<< "$shas"
|
|
333
|
+
return 0
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# -----------------------------------------------------------------------------
|
|
337
|
+
# Status write helpers
|
|
338
|
+
# -----------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
write_status() {
|
|
341
|
+
local file="$1" new_status="$2"
|
|
342
|
+
"$WITH_LOCK_SCRIPT" "$file" -- bash "$UPDATE_FRONTMATTER_SCRIPT" "$file" status "$new_status"
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Writes status: Deployed to Prod AND date_deployed_prod inside the SAME
|
|
346
|
+
# locked window -- one with-lock.sh call wrapping a bash -c that chains both
|
|
347
|
+
# update-task-frontmatter.sh invocations, never two separately-locked calls
|
|
348
|
+
# (so no observer can see the status flip without the date already present).
|
|
349
|
+
write_deployed_to_prod() {
|
|
350
|
+
local file="$1" today="$2"
|
|
351
|
+
# update-task-frontmatter.sh does not carry an execute bit in this repo
|
|
352
|
+
# (same as mark-merged.sh's own documented reasoning) -- invoke it via
|
|
353
|
+
# `bash "$1"` inside the bash -c, not a direct exec.
|
|
354
|
+
"$WITH_LOCK_SCRIPT" "$file" -- bash -c \
|
|
355
|
+
'bash "$1" "$2" status "Deployed to Prod" && bash "$1" "$2" date_deployed_prod "$3"' \
|
|
356
|
+
_ "$UPDATE_FRONTMATTER_SCRIPT" "$file" "$today"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# -----------------------------------------------------------------------------
|
|
360
|
+
# Marker file read/write (python3-only, matching validate-board.sh's own
|
|
361
|
+
# unconditional python3 dependency -- no jq fallback needed here).
|
|
362
|
+
# -----------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
advance_marker() {
|
|
365
|
+
# Filter out empty-string elements before counting: the call site passes
|
|
366
|
+
# "${RECONCILED_OK[@]:-}", which (per bash's :- expansion on an empty
|
|
367
|
+
# array) yields a single empty-string argument rather than zero
|
|
368
|
+
# arguments when RECONCILED_OK has no elements. Without this filter, that
|
|
369
|
+
# phantom empty string would be miscounted as one real tag and appended
|
|
370
|
+
# to reconciled_tags as "" (an empty version-name entry).
|
|
371
|
+
local -a tags=()
|
|
372
|
+
local t
|
|
373
|
+
for t in "$@"; do
|
|
374
|
+
[ -n "$t" ] || continue
|
|
375
|
+
tags+=("$t")
|
|
376
|
+
done
|
|
377
|
+
[ "${#tags[@]}" -gt 0 ] || { log "no tag(s) succeeded this run -- marker not advanced"; return 0; }
|
|
378
|
+
|
|
379
|
+
mkdir -p "$(dirname "$MARKER_FILE")"
|
|
380
|
+
|
|
381
|
+
if ! "$WITH_LOCK_SCRIPT" "$MARKER_FILE" -- python3 -c '
|
|
382
|
+
import json, sys
|
|
383
|
+
|
|
384
|
+
marker_file = sys.argv[1]
|
|
385
|
+
new_tags = sys.argv[2:]
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
with open(marker_file) as fh:
|
|
389
|
+
data = json.load(fh)
|
|
390
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
391
|
+
data = {}
|
|
392
|
+
|
|
393
|
+
existing = data.get("reconciled_tags") or []
|
|
394
|
+
merged = list(existing)
|
|
395
|
+
for t in new_tags:
|
|
396
|
+
if t not in merged:
|
|
397
|
+
merged.append(t)
|
|
398
|
+
data["reconciled_tags"] = merged
|
|
399
|
+
|
|
400
|
+
with open(marker_file, "w") as fh:
|
|
401
|
+
json.dump(data, fh, indent=2)
|
|
402
|
+
fh.write("\n")
|
|
403
|
+
' "$MARKER_FILE" "${tags[@]}"; then
|
|
404
|
+
warn "failed to advance $MARKER_FILE under lock -- reconciled tag(s) not recorded (will be reprocessed next run): ${tags[*]}"
|
|
405
|
+
return 1
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
log "advanced $MARKER_FILE: reconciled_tags += ${tags[*]}"
|
|
409
|
+
return 0
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# -----------------------------------------------------------------------------
|
|
413
|
+
# Ticket enumeration + per-tag processing
|
|
414
|
+
# -----------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
TAG_HAD_ERROR=0
|
|
417
|
+
STAGE_COUNT=0
|
|
418
|
+
PROD_COUNT=0
|
|
419
|
+
DATE_TODAY="$(date -u +%Y-%m-%d)"
|
|
420
|
+
|
|
421
|
+
# Processes every eligible ticket in $1 (TASKS_DIR or STORIES_DIR) against a
|
|
422
|
+
# single tag ($2=tag_type, $3=version, $4=tag_name, $5=sha). Sets
|
|
423
|
+
# TAG_HAD_ERROR=1 (never reset here -- caller resets per tag) if any write
|
|
424
|
+
# fails.
|
|
425
|
+
process_ticket_dir_for_tag() {
|
|
426
|
+
local dir="$1" tag_type="$2" version="$3" tag_name="$4" sha="$5"
|
|
427
|
+
[ -d "$dir" ] || return 0
|
|
428
|
+
|
|
429
|
+
local file id status shas touched
|
|
430
|
+
while IFS= read -r -d '' file; do
|
|
431
|
+
id="$(trim "$(read_frontmatter_field "$file" "id")")"
|
|
432
|
+
status="$(trim "$(read_frontmatter_field "$file" "status")")"
|
|
433
|
+
|
|
434
|
+
[ -n "$id" ] || { log "WARNING: no 'id' frontmatter field found in $file -- skipping"; continue; }
|
|
435
|
+
|
|
436
|
+
case "$status" in
|
|
437
|
+
Publicized|"Deployed to Stage") ;;
|
|
438
|
+
*) continue ;; # not eligible for Deployed to Stage/Prod consideration
|
|
439
|
+
esac
|
|
440
|
+
|
|
441
|
+
shas="$(commits_for_ticket "$id")"
|
|
442
|
+
if [ -z "$shas" ]; then
|
|
443
|
+
log "$id: zero matched EST-tagged commits -- left unchanged (tag $tag_name)"
|
|
444
|
+
continue
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
touched="$(collect_touched_files "$shas")"
|
|
448
|
+
log "$id: ${shas//$'\n'/,} matched commit(s), touching: ${touched:-<none>} (tag $tag_name)"
|
|
449
|
+
|
|
450
|
+
if ! all_ancestors_of "$shas" "$sha"; then
|
|
451
|
+
log "$id: not fully covered by tag $tag_name (some matched commit is not an ancestor of $sha) -- left unchanged"
|
|
452
|
+
continue
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
if [ "$tag_type" = "stage" ]; then
|
|
456
|
+
if [ "$status" = "Publicized" ]; then
|
|
457
|
+
log "$id: covered by stage tag $tag_name -- writing status: Deployed to Stage ($file)"
|
|
458
|
+
if write_status "$file" "Deployed to Stage"; then
|
|
459
|
+
STAGE_COUNT=$((STAGE_COUNT + 1))
|
|
460
|
+
else
|
|
461
|
+
log "ERROR: failed to write status: Deployed to Stage for $id ($file)"
|
|
462
|
+
TAG_HAD_ERROR=1
|
|
463
|
+
fi
|
|
464
|
+
else
|
|
465
|
+
log "$id: already at '$status' -- unchanged (stage tag $tag_name, idempotent no-op)"
|
|
466
|
+
fi
|
|
467
|
+
else
|
|
468
|
+
# tag_type == prod
|
|
469
|
+
if [ "$status" = "Publicized" ] || [ "$status" = "Deployed to Stage" ]; then
|
|
470
|
+
log "$id: covered by prod tag $tag_name -- writing status: Deployed to Prod + date_deployed_prod ($file)"
|
|
471
|
+
if write_deployed_to_prod "$file" "$DATE_TODAY"; then
|
|
472
|
+
PROD_COUNT=$((PROD_COUNT + 1))
|
|
473
|
+
else
|
|
474
|
+
log "ERROR: failed to write status: Deployed to Prod / date_deployed_prod for $id ($file)"
|
|
475
|
+
TAG_HAD_ERROR=1
|
|
476
|
+
fi
|
|
477
|
+
else
|
|
478
|
+
log "$id: already at '$status' -- unchanged (prod tag $tag_name, idempotent no-op)"
|
|
479
|
+
fi
|
|
480
|
+
fi
|
|
481
|
+
done < <(find "$dir" -maxdepth 1 -type f -name '*.md' -print0 | sort -z)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
RECONCILED_OK=()
|
|
485
|
+
ANY_ERROR_OCCURRED=0
|
|
486
|
+
|
|
487
|
+
process_tag_line() {
|
|
488
|
+
local line="$1"
|
|
489
|
+
local tag_type version tag_name sha
|
|
490
|
+
IFS=$'\t' read -r tag_type version tag_name sha <<< "$line"
|
|
491
|
+
|
|
492
|
+
TAG_HAD_ERROR=0
|
|
493
|
+
process_ticket_dir_for_tag "$TASKS_DIR" "$tag_type" "$version" "$tag_name" "$sha"
|
|
494
|
+
process_ticket_dir_for_tag "$STORIES_DIR" "$tag_type" "$version" "$tag_name" "$sha"
|
|
495
|
+
|
|
496
|
+
if [ "$TAG_HAD_ERROR" -eq 0 ]; then
|
|
497
|
+
RECONCILED_OK+=("$tag_name")
|
|
498
|
+
else
|
|
499
|
+
ANY_ERROR_OCCURRED=1
|
|
500
|
+
log "tag $tag_name ($tag_type $version) had one or more write failures -- will NOT be marked reconciled this run"
|
|
501
|
+
fi
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for line in "${STAGE_LINES[@]:-}"; do
|
|
505
|
+
[ -n "$line" ] || continue
|
|
506
|
+
process_tag_line "$line"
|
|
507
|
+
done
|
|
508
|
+
|
|
509
|
+
for line in "${PROD_LINES[@]:-}"; do
|
|
510
|
+
[ -n "$line" ] || continue
|
|
511
|
+
process_tag_line "$line"
|
|
512
|
+
done
|
|
513
|
+
|
|
514
|
+
log "processed ${#STAGE_LINES[@]} stage tag(s), ${#PROD_LINES[@]} prod tag(s); $STAGE_COUNT ticket(s) set to Deployed to Stage, $PROD_COUNT ticket(s) set to Deployed to Prod"
|
|
515
|
+
|
|
516
|
+
# -----------------------------------------------------------------------------
|
|
517
|
+
# Advance the marker -- only for tags that completed with zero write
|
|
518
|
+
# failures. A tag with a failure is simply omitted, so a future run
|
|
519
|
+
# re-discovers and retries it (compute-deploy-reconcile.sh reports any tag
|
|
520
|
+
# not yet in reconciled_tags).
|
|
521
|
+
# -----------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
if ! advance_marker "${RECONCILED_OK[@]:-}"; then
|
|
524
|
+
ANY_ERROR_OCCURRED=1
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
if [ "$ANY_ERROR_OCCURRED" -ne 0 ]; then
|
|
528
|
+
log "completed with one or more failures -- see ERROR/WARNING lines above; affected tag(s) will be retried on a future run"
|
|
529
|
+
exit 1
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
exit 0
|