@indigoai-us/hq-cli 5.345.114 → 5.345.116
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/CHANGELOG.md +13 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
- package/dist/commands/bot-jobs.d.ts +1 -11
- package/dist/commands/bot-jobs.js +13 -34
- package/dist/commands/core-audit-log-native.js +1 -1
- package/dist/commands/core-checkpoint-native.d.ts +27 -0
- package/dist/commands/core-checkpoint-native.js +85 -2
- package/dist/commands/core-dm-bind-native.js +1 -1
- package/dist/commands/core-git-pack-extension-check-native.js +1 -1
- package/dist/commands/core-job-native-utils.d.ts +2 -2
- package/dist/commands/core-job-native-utils.js +3 -11
- package/dist/commands/core-job-notify-native.js +1 -2
- package/dist/commands/core-job-probe-native.d.ts +2 -3
- package/dist/commands/core-job-probe-native.js +14 -22
- package/dist/commands/core-job-remediate-native.js +1 -2
- package/dist/commands/core-job-run-native.js +58 -54
- package/dist/commands/core-jobs-native.d.ts +1 -3
- package/dist/commands/core-jobs-native.js +16 -271
- package/dist/commands/core-jobs-schedule.d.ts +7 -0
- package/dist/commands/core-jobs-schedule.js +243 -0
- package/dist/commands/core-native-utilities.d.ts +2 -3
- package/dist/commands/core-native-utilities.js +2 -19
- package/dist/commands/core-pin-native.js +1 -1
- package/dist/commands/core-refresh-vault-access-native.js +1 -1
- package/dist/commands/core-resolve-company-command.js +2 -17
- package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
- package/dist/commands/core.js +1 -17
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +0 -38
- package/dist/lib/daemon/catalog.d.ts +0 -2
- package/dist/lib/daemon/catalog.js +3 -6
- package/dist/lib/daemon/run.js +0 -7
- package/dist/lib/scan-packages/index.js +6 -0
- package/dist/lib/workers-registry/index.d.ts +16 -0
- package/dist/lib/workers-registry/index.js +1 -1
- package/dist/utils/resident-package-preload.js +2 -7
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/audit-log.sh +0 -384
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
- package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
- package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
- package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
- package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
- package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
- package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
- package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
- package/dist/commands/core-job-post-exit-assets.js +0 -37
- package/dist/commands/core-native-jobs-flag.d.ts +0 -4
- package/dist/commands/core-native-jobs-flag.js +0 -7
- package/dist/commands/core-native-utilities-flag.d.ts +0 -4
- package/dist/commands/core-native-utilities-flag.js +0 -7
|
@@ -1,836 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hq-core: public
|
|
3
|
-
# outpost-jobs-reconcile.sh — registry YAML → systemd user timers (US-004).
|
|
4
|
-
#
|
|
5
|
-
# Reads personal/jobs/*.yaml and companies/*/jobs/*.yaml from the box HQ tree,
|
|
6
|
-
# validates each with jobs-validate.sh, and materializes disposable units:
|
|
7
|
-
# ~/.config/systemd/user/hq-job-{id}.service
|
|
8
|
-
# ~/.config/systemd/user/hq-job-{id}.timer
|
|
9
|
-
# with Persistent=true and RandomizedDelaySec. Timers are armed only when
|
|
10
|
-
# hq-pro jobs status readiness=ready (box cache from probe ingest, or live
|
|
11
|
-
# GET when available). Disabled/deleted jobs have units removed.
|
|
12
|
-
#
|
|
13
|
-
# Company-scoped jobs materialize only when job.owner matches this Outpost's
|
|
14
|
-
# owner identity (creator's box only in v1).
|
|
15
|
-
#
|
|
16
|
-
# Usage:
|
|
17
|
-
# core/scripts/outpost-jobs-reconcile.sh [options]
|
|
18
|
-
# core/scripts/outpost-jobs-reconcile.sh --after-sync # post-sync entrypoint
|
|
19
|
-
#
|
|
20
|
-
# Options:
|
|
21
|
-
# --hq-root DIR HQ tree root (default: HQ_ROOT or repo root above core/)
|
|
22
|
-
# --dry-run Write units to unit dir / print plan; skip systemctl
|
|
23
|
-
# --no-probe Do not probe non-ready jobs
|
|
24
|
-
# --probe-only Probe non-ready jobs then exit (no unit changes)
|
|
25
|
-
# --ensure-hook Install personal/hooks/Stop/90-outpost-jobs-reconcile.sh
|
|
26
|
-
# --after-sync Alias for default reconcile + probe path (sync Stop hook)
|
|
27
|
-
# -h, --help
|
|
28
|
-
#
|
|
29
|
-
# Env / overrides (tests):
|
|
30
|
-
# HQ_ROOT, HOME, HQ_JOB_UNIT_DIR, HQ_JOB_SYSTEMCTL, HQ_JOB_STATUS_CACHE_DIR
|
|
31
|
-
# HQ_JOB_OWNER_EMAIL, HQ_JOB_OWNER_UID, OUTPOST_USER_ID, OUTPOST_ID
|
|
32
|
-
# HQ_JOB_RANDOMIZE_SEC (default 60), HQ_JOB_RECONCILE_STATE
|
|
33
|
-
# HQ_PRO_API_URL / HQ_API_URL / HQ_VAULT_API_URL, HQ_ACCESS_TOKEN (optional GET)
|
|
34
|
-
#
|
|
35
|
-
# Exit: 0 success; 1 partial failures; 2 usage/config.
|
|
36
|
-
|
|
37
|
-
set -euo pipefail
|
|
38
|
-
|
|
39
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
40
|
-
HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
41
|
-
VALIDATE="$SCRIPT_DIR/jobs-validate.sh"
|
|
42
|
-
PROBE="$SCRIPT_DIR/hq-job-probe.sh"
|
|
43
|
-
|
|
44
|
-
DRY_RUN=0
|
|
45
|
-
NO_PROBE=0
|
|
46
|
-
PROBE_ONLY=0
|
|
47
|
-
ENSURE_HOOK=0
|
|
48
|
-
|
|
49
|
-
usage() {
|
|
50
|
-
sed -n '3,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
die() {
|
|
54
|
-
echo "outpost-jobs-reconcile: $*" >&2
|
|
55
|
-
exit 2
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
log() {
|
|
59
|
-
echo "outpost-jobs-reconcile: $*" >&2
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
while [ $# -gt 0 ]; do
|
|
63
|
-
case "$1" in
|
|
64
|
-
--hq-root)
|
|
65
|
-
HQ_ROOT="${2:-}"
|
|
66
|
-
[ -n "$HQ_ROOT" ] || die "--hq-root requires a directory"
|
|
67
|
-
shift 2
|
|
68
|
-
;;
|
|
69
|
-
--dry-run)
|
|
70
|
-
DRY_RUN=1
|
|
71
|
-
shift
|
|
72
|
-
;;
|
|
73
|
-
--no-probe)
|
|
74
|
-
NO_PROBE=1
|
|
75
|
-
shift
|
|
76
|
-
;;
|
|
77
|
-
--probe-only)
|
|
78
|
-
PROBE_ONLY=1
|
|
79
|
-
shift
|
|
80
|
-
;;
|
|
81
|
-
--ensure-hook)
|
|
82
|
-
ENSURE_HOOK=1
|
|
83
|
-
shift
|
|
84
|
-
;;
|
|
85
|
-
--after-sync)
|
|
86
|
-
shift
|
|
87
|
-
;;
|
|
88
|
-
-h|--help)
|
|
89
|
-
usage
|
|
90
|
-
exit 0
|
|
91
|
-
;;
|
|
92
|
-
-*)
|
|
93
|
-
die "unknown option: $1"
|
|
94
|
-
;;
|
|
95
|
-
*)
|
|
96
|
-
die "unexpected argument: $1"
|
|
97
|
-
;;
|
|
98
|
-
esac
|
|
99
|
-
done
|
|
100
|
-
|
|
101
|
-
# The shell script is also bundled inside hq-cli. Its own SCRIPT_DIR then
|
|
102
|
-
# points into the npm package, while job units must always target the live
|
|
103
|
-
# scaffold forwarder so they survive CLI upgrades and Node switches.
|
|
104
|
-
RUNNER="$HQ_ROOT/core/scripts/hq-job-run.sh"
|
|
105
|
-
|
|
106
|
-
command -v yq >/dev/null 2>&1 || die "yq is required (mikefarah/yq)"
|
|
107
|
-
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
108
|
-
[ -x "$VALIDATE" ] || [ -f "$VALIDATE" ] || die "missing jobs-validate.sh"
|
|
109
|
-
[ -f "$RUNNER" ] || die "missing hq-job-run.sh"
|
|
110
|
-
[ -f "$PROBE" ] || die "missing hq-job-probe.sh"
|
|
111
|
-
|
|
112
|
-
UNIT_DIR="${HQ_JOB_UNIT_DIR:-${HOME}/.config/systemd/user}"
|
|
113
|
-
STATUS_CACHE_DIR="${HQ_JOB_STATUS_CACHE_DIR:-${HOME}/.hq/jobs/status-cache}"
|
|
114
|
-
STATE_DIR="${HQ_JOB_RECONCILE_STATE:-${HOME}/.hq/jobs/reconcile}"
|
|
115
|
-
SYSTEMCTL_BIN="${HQ_JOB_SYSTEMCTL:-systemctl}"
|
|
116
|
-
RANDOMIZE_SEC="${HQ_JOB_RANDOMIZE_SEC:-60}"
|
|
117
|
-
mkdir -p "$UNIT_DIR" "$STATUS_CACHE_DIR" "$STATE_DIR"
|
|
118
|
-
|
|
119
|
-
iso_now() {
|
|
120
|
-
date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|
|
121
|
-
|| date -u +"%Y-%m-%dT%H:%M:%S+00:00"
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
api_base() {
|
|
125
|
-
local base
|
|
126
|
-
base="${HQ_PRO_API_URL:-${HQ_API_URL:-${HQ_VAULT_API_URL:-https://hqapi.hq.computer}}}"
|
|
127
|
-
printf '%s' "${base%/}"
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
# --- owner identity (company jobs: creator's Outpost only) -------------------
|
|
131
|
-
|
|
132
|
-
resolve_box_owners() {
|
|
133
|
-
# Emits newline-separated owner identity tokens this box may claim.
|
|
134
|
-
local tokens=""
|
|
135
|
-
add_token() {
|
|
136
|
-
local t="$1"
|
|
137
|
-
[ -n "$t" ] && [ "$t" != "null" ] || return 0
|
|
138
|
-
case $'\n'"$tokens"$'\n' in
|
|
139
|
-
*$'\n'"$t"$'\n'*) ;;
|
|
140
|
-
*) tokens="${tokens:+${tokens}$'\n'}${t}" ;;
|
|
141
|
-
esac
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
add_token "${HQ_JOB_OWNER_EMAIL:-}"
|
|
145
|
-
add_token "${HQ_JOB_OWNER_UID:-}"
|
|
146
|
-
add_token "${OUTPOST_USER_ID:-}"
|
|
147
|
-
|
|
148
|
-
if command -v hq >/dev/null 2>&1; then
|
|
149
|
-
local who
|
|
150
|
-
who="$(hq whoami --json 2>/dev/null || hq whoami 2>/dev/null || true)"
|
|
151
|
-
if [ -n "$who" ]; then
|
|
152
|
-
local email uid
|
|
153
|
-
email="$(printf '%s' "$who" | jq -r '.email // .user.email // empty' 2>/dev/null || true)"
|
|
154
|
-
uid="$(printf '%s' "$who" | jq -r '.personUid // .person_uid // .uid // empty' 2>/dev/null || true)"
|
|
155
|
-
if [ -z "$email" ]; then
|
|
156
|
-
email="$(printf '%s' "$who" | sed -n 's/.*Email:[[:space:]]*//Ip;s/.*email[[:space:]]*[:=][[:space:]]*//Ip' | head -1 | tr -d '[:space:]')"
|
|
157
|
-
fi
|
|
158
|
-
add_token "$email"
|
|
159
|
-
add_token "$uid"
|
|
160
|
-
fi
|
|
161
|
-
fi
|
|
162
|
-
|
|
163
|
-
# Common on-box identity files.
|
|
164
|
-
if [ -f /etc/outpost/codex-identity.env ]; then
|
|
165
|
-
# shellcheck disable=SC1091
|
|
166
|
-
. /etc/outpost/codex-identity.env 2>/dev/null || true
|
|
167
|
-
add_token "${OUTPOST_USER_ID:-}"
|
|
168
|
-
add_token "${OUTPOST_OWNER_EMAIL:-}"
|
|
169
|
-
add_token "${OWNER_EMAIL:-}"
|
|
170
|
-
fi
|
|
171
|
-
|
|
172
|
-
printf '%s' "$tokens"
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
owner_matches() {
|
|
176
|
-
local job_owner="$1"
|
|
177
|
-
local box_owners="$2"
|
|
178
|
-
[ -n "$job_owner" ] || return 1
|
|
179
|
-
local t
|
|
180
|
-
while IFS= read -r t; do
|
|
181
|
-
[ -n "$t" ] || continue
|
|
182
|
-
if [ "$t" = "$job_owner" ]; then
|
|
183
|
-
return 0
|
|
184
|
-
fi
|
|
185
|
-
# Case-insensitive email compare
|
|
186
|
-
if printf '%s' "$t" | grep -qi '@' \
|
|
187
|
-
&& [ "$(printf '%s' "$t" | tr '[:upper:]' '[:lower:]')" = "$(printf '%s' "$job_owner" | tr '[:upper:]' '[:lower:]')" ]; then
|
|
188
|
-
return 0
|
|
189
|
-
fi
|
|
190
|
-
done <<EOF
|
|
191
|
-
$box_owners
|
|
192
|
-
EOF
|
|
193
|
-
return 1
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# --- readiness cache ---------------------------------------------------------
|
|
197
|
-
|
|
198
|
-
status_cache_path() {
|
|
199
|
-
printf '%s/%s.json' "$STATUS_CACHE_DIR" "$1"
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
read_cached_readiness() {
|
|
203
|
-
local id="$1" path
|
|
204
|
-
path="$(status_cache_path "$id")"
|
|
205
|
-
if [ -f "$path" ]; then
|
|
206
|
-
yq -r '.readiness // "unknown"' "$path" 2>/dev/null || echo unknown
|
|
207
|
-
return
|
|
208
|
-
fi
|
|
209
|
-
# Fall back to probe last-attempt marker when it recorded a successful ingest.
|
|
210
|
-
local probe_marker="${HOME}/.hq/jobs/probes/${id}/last-attempt.json"
|
|
211
|
-
if [ -f "$probe_marker" ]; then
|
|
212
|
-
local reported
|
|
213
|
-
reported="$(jq -r '.reported_readiness // empty' "$probe_marker" 2>/dev/null || true)"
|
|
214
|
-
if [ -n "$reported" ] && [ "$reported" != "null" ]; then
|
|
215
|
-
printf '%s' "$reported"
|
|
216
|
-
return
|
|
217
|
-
fi
|
|
218
|
-
fi
|
|
219
|
-
printf 'unknown'
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
write_status_cache() {
|
|
223
|
-
local id="$1" readiness="$2" source="${3:-reconcile}"
|
|
224
|
-
local path
|
|
225
|
-
path="$(status_cache_path "$id")"
|
|
226
|
-
jq -nc \
|
|
227
|
-
--arg job_id "$id" \
|
|
228
|
-
--arg readiness "$readiness" \
|
|
229
|
-
--arg updated_at "$(iso_now)" \
|
|
230
|
-
--arg source "$source" \
|
|
231
|
-
'{job_id:$job_id,readiness:$readiness,updated_at:$updated_at,source:$source}' \
|
|
232
|
-
>"$path"
|
|
233
|
-
chmod 600 "$path" 2>/dev/null || true
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
# Optional live GET when HQ_ACCESS_TOKEN is present (laptop JWT on box is rare).
|
|
237
|
-
fetch_live_readiness() {
|
|
238
|
-
local id="$1"
|
|
239
|
-
[ -n "${HQ_ACCESS_TOKEN:-}" ] || return 1
|
|
240
|
-
local url curl_bin http body tmp
|
|
241
|
-
url="$(api_base)/outpost/jobs/status/${id}"
|
|
242
|
-
curl_bin="${HQ_JOB_PROBE_CURL:-curl}"
|
|
243
|
-
command -v "$curl_bin" >/dev/null 2>&1 || return 1
|
|
244
|
-
tmp="$(mktemp "${TMPDIR:-/tmp}/hq-job-status.XXXXXX")"
|
|
245
|
-
http=0
|
|
246
|
-
set +e
|
|
247
|
-
"$curl_bin" -sS -m 15 \
|
|
248
|
-
-H "authorization: Bearer ${HQ_ACCESS_TOKEN}" \
|
|
249
|
-
-H "accept: application/json" \
|
|
250
|
-
-o "$tmp" \
|
|
251
|
-
-w '%{http_code}' \
|
|
252
|
-
"$url" >"${tmp}.code"
|
|
253
|
-
set -e
|
|
254
|
-
http="$(cat "${tmp}.code" 2>/dev/null || echo 0)"
|
|
255
|
-
body="$(cat "$tmp" 2>/dev/null || true)"
|
|
256
|
-
rm -f "$tmp" "${tmp}.code"
|
|
257
|
-
if [ "$http" -ge 200 ] && [ "$http" -lt 300 ]; then
|
|
258
|
-
local r
|
|
259
|
-
r="$(printf '%s' "$body" | jq -r '.status.readiness // .readiness // empty' 2>/dev/null || true)"
|
|
260
|
-
if [ -n "$r" ]; then
|
|
261
|
-
write_status_cache "$id" "$r" "api-get"
|
|
262
|
-
printf '%s' "$r"
|
|
263
|
-
return 0
|
|
264
|
-
fi
|
|
265
|
-
fi
|
|
266
|
-
return 1
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
job_readiness() {
|
|
270
|
-
local id="$1" r
|
|
271
|
-
if r="$(fetch_live_readiness "$id")"; then
|
|
272
|
-
printf '%s' "$r"
|
|
273
|
-
return
|
|
274
|
-
fi
|
|
275
|
-
read_cached_readiness "$id"
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
# --- cron → systemd OnCalendar -----------------------------------------------
|
|
279
|
-
|
|
280
|
-
# Map cron DOW (0/7=Sun … 6=Sat) to systemd weekday names.
|
|
281
|
-
cron_dow_to_systemd() {
|
|
282
|
-
local field="$1"
|
|
283
|
-
case "$field" in
|
|
284
|
-
'*'|'*/1') printf ''; return ;;
|
|
285
|
-
esac
|
|
286
|
-
local out="" part IFS=','
|
|
287
|
-
# shellcheck disable=SC2086
|
|
288
|
-
set -f
|
|
289
|
-
set -- $field
|
|
290
|
-
set +f
|
|
291
|
-
for part in "$@"; do
|
|
292
|
-
local mapped
|
|
293
|
-
mapped="$(cron_dow_token_to_systemd "$part")" || return 1
|
|
294
|
-
[ -n "$mapped" ] || continue
|
|
295
|
-
out="${out:+$out,}$mapped"
|
|
296
|
-
done
|
|
297
|
-
printf '%s' "$out"
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
cron_dow_token_to_systemd() {
|
|
301
|
-
local tok="$1"
|
|
302
|
-
# ranges like 1-5 or Mon-Fri
|
|
303
|
-
if printf '%s' "$tok" | grep -Eq '^[0-7]+-[0-7]+$'; then
|
|
304
|
-
local a b
|
|
305
|
-
a="${tok%-*}"; b="${tok#*-}"
|
|
306
|
-
printf '%s..%s' "$(cron_dow_num_to_name "$a")" "$(cron_dow_num_to_name "$b")"
|
|
307
|
-
return
|
|
308
|
-
fi
|
|
309
|
-
case "$(printf '%s' "$tok" | tr '[:upper:]' '[:lower:]')" in
|
|
310
|
-
sun|0|7) printf 'Sun' ;;
|
|
311
|
-
mon|1) printf 'Mon' ;;
|
|
312
|
-
tue|2) printf 'Tue' ;;
|
|
313
|
-
wed|3) printf 'Wed' ;;
|
|
314
|
-
thu|4) printf 'Thu' ;;
|
|
315
|
-
fri|5) printf 'Fri' ;;
|
|
316
|
-
sat|6) printf 'Sat' ;;
|
|
317
|
-
mon..fri|1-5) printf 'Mon..Fri' ;;
|
|
318
|
-
*) return 1 ;;
|
|
319
|
-
esac
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
cron_dow_num_to_name() {
|
|
323
|
-
case "$1" in
|
|
324
|
-
0|7) printf 'Sun' ;;
|
|
325
|
-
1) printf 'Mon' ;;
|
|
326
|
-
2) printf 'Tue' ;;
|
|
327
|
-
3) printf 'Wed' ;;
|
|
328
|
-
4) printf 'Thu' ;;
|
|
329
|
-
5) printf 'Fri' ;;
|
|
330
|
-
6) printf 'Sat' ;;
|
|
331
|
-
*) return 1 ;;
|
|
332
|
-
esac
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
pad2() {
|
|
336
|
-
local n="$1"
|
|
337
|
-
if [ "${#n}" -eq 1 ]; then
|
|
338
|
-
printf '0%s' "$n"
|
|
339
|
-
else
|
|
340
|
-
printf '%s' "$n"
|
|
341
|
-
fi
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
# Convert a single cron field that is a fixed number or */n or a-b into
|
|
345
|
-
# systemd calendar fragment for hour/minute. Returns empty for '*'.
|
|
346
|
-
cron_time_field() {
|
|
347
|
-
local field="$1"
|
|
348
|
-
case "$field" in
|
|
349
|
-
'*') printf ''; return ;;
|
|
350
|
-
*/*)
|
|
351
|
-
local step="${field#*/}"
|
|
352
|
-
printf '*/%s' "$step"
|
|
353
|
-
return
|
|
354
|
-
;;
|
|
355
|
-
*-*)
|
|
356
|
-
printf '%s' "$field"
|
|
357
|
-
return
|
|
358
|
-
;;
|
|
359
|
-
*)
|
|
360
|
-
if printf '%s' "$field" | grep -Eq '^[0-9]+$'; then
|
|
361
|
-
pad2 "$field"
|
|
362
|
-
return
|
|
363
|
-
fi
|
|
364
|
-
printf '%s' "$field"
|
|
365
|
-
;;
|
|
366
|
-
esac
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
cron_to_oncalendar() {
|
|
370
|
-
# stdin unused; args: minute hour dom month dow → prints OnCalendar value
|
|
371
|
-
local minute="$1" hour="$2" dom="$3" month="$4" dow="$5"
|
|
372
|
-
local dow_s date_s time_s h m
|
|
373
|
-
|
|
374
|
-
dow_s="$(cron_dow_to_systemd "$dow" || true)"
|
|
375
|
-
|
|
376
|
-
# Date part: year-month-day → *-month-day
|
|
377
|
-
local mon_s day_s
|
|
378
|
-
case "$month" in
|
|
379
|
-
'*') mon_s='*' ;;
|
|
380
|
-
*) mon_s="$(pad2 "$month" 2>/dev/null || printf '%s' "$month")" ;;
|
|
381
|
-
esac
|
|
382
|
-
case "$dom" in
|
|
383
|
-
'*') day_s='*' ;;
|
|
384
|
-
*) day_s="$(pad2 "$dom" 2>/dev/null || printf '%s' "$dom")" ;;
|
|
385
|
-
esac
|
|
386
|
-
date_s="*-${mon_s}-${day_s}"
|
|
387
|
-
|
|
388
|
-
m="$(cron_time_field "$minute")"
|
|
389
|
-
h="$(cron_time_field "$hour")"
|
|
390
|
-
[ -n "$m" ] || m='00'
|
|
391
|
-
[ -n "$h" ] || h='*'
|
|
392
|
-
# Normalize pure numbers already padded; handle hour=*
|
|
393
|
-
if [ "$h" = '*' ]; then
|
|
394
|
-
time_s="*:${m}:00"
|
|
395
|
-
elif printf '%s' "$h" | grep -q '/'; then
|
|
396
|
-
# e.g. */6 → 0/6
|
|
397
|
-
local step="${h#*/}"
|
|
398
|
-
time_s="0/${step}:${m}:00"
|
|
399
|
-
else
|
|
400
|
-
time_s="${h}:${m}:00"
|
|
401
|
-
fi
|
|
402
|
-
|
|
403
|
-
if [ -n "$dow_s" ]; then
|
|
404
|
-
printf '%s %s %s' "$dow_s" "$date_s" "$time_s"
|
|
405
|
-
else
|
|
406
|
-
printf '%s %s' "$date_s" "$time_s"
|
|
407
|
-
fi
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
# --- unit writers ------------------------------------------------------------
|
|
411
|
-
|
|
412
|
-
unit_service_path() { printf '%s/hq-job-%s.service' "$UNIT_DIR" "$1"; }
|
|
413
|
-
unit_timer_path() { printf '%s/hq-job-%s.timer' "$UNIT_DIR" "$1"; }
|
|
414
|
-
|
|
415
|
-
normalize_service_path() {
|
|
416
|
-
local remaining="$1" entry existing normalized="" has_more seen entry_count=0 index
|
|
417
|
-
local -a entries=()
|
|
418
|
-
while :; do
|
|
419
|
-
if [[ "$remaining" == *:* ]]; then
|
|
420
|
-
entry="${remaining%%:*}"
|
|
421
|
-
remaining="${remaining#*:}"
|
|
422
|
-
has_more=1
|
|
423
|
-
else
|
|
424
|
-
entry="$remaining"
|
|
425
|
-
remaining=""
|
|
426
|
-
has_more=0
|
|
427
|
-
fi
|
|
428
|
-
case "$entry" in
|
|
429
|
-
/*)
|
|
430
|
-
seen=0
|
|
431
|
-
for ((index = 0; index < entry_count; index++)); do
|
|
432
|
-
existing="${entries[index]}"
|
|
433
|
-
if [ "$existing" = "$entry" ]; then
|
|
434
|
-
seen=1
|
|
435
|
-
break
|
|
436
|
-
fi
|
|
437
|
-
done
|
|
438
|
-
if [ "$seen" -eq 0 ]; then
|
|
439
|
-
entries+=("$entry")
|
|
440
|
-
entry_count=$((entry_count + 1))
|
|
441
|
-
fi
|
|
442
|
-
;;
|
|
443
|
-
esac
|
|
444
|
-
[ "$has_more" -eq 1 ] || break
|
|
445
|
-
done
|
|
446
|
-
if [ "$entry_count" -gt 0 ]; then
|
|
447
|
-
local IFS=:
|
|
448
|
-
normalized="${entries[*]}"
|
|
449
|
-
fi
|
|
450
|
-
printf '%s' "$normalized"
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
unit_service_path_value() {
|
|
454
|
-
local service_path hq_bin npm_prefix hq_bin_dir
|
|
455
|
-
service_path="${PATH:-/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}"
|
|
456
|
-
service_path="$(normalize_service_path "$service_path")"
|
|
457
|
-
hq_bin=""
|
|
458
|
-
if [ -n "$service_path" ]; then
|
|
459
|
-
hq_bin="$(PATH="$service_path" type -P hq 2>/dev/null || true)"
|
|
460
|
-
fi
|
|
461
|
-
if [ -z "$hq_bin" ] && PATH="$service_path" command -v npm >/dev/null 2>&1; then
|
|
462
|
-
npm_prefix="$(PATH="$service_path" npm prefix -g 2>/dev/null || true)"
|
|
463
|
-
if [ -n "$npm_prefix" ] && [ -x "$npm_prefix/bin/hq" ]; then
|
|
464
|
-
hq_bin="$npm_prefix/bin/hq"
|
|
465
|
-
fi
|
|
466
|
-
fi
|
|
467
|
-
if [ -n "$hq_bin" ] && [ -x "$hq_bin" ]; then
|
|
468
|
-
hq_bin_dir="$(cd "$(dirname "$hq_bin")" 2>/dev/null && pwd -P || true)"
|
|
469
|
-
if [ -n "$hq_bin_dir" ]; then
|
|
470
|
-
case ":$service_path:" in
|
|
471
|
-
*":$hq_bin_dir:"*) ;;
|
|
472
|
-
*) service_path="$hq_bin_dir:$service_path" ;;
|
|
473
|
-
esac
|
|
474
|
-
fi
|
|
475
|
-
fi
|
|
476
|
-
printf '%s' "$service_path"
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
systemd_quote() {
|
|
480
|
-
local value="$1"
|
|
481
|
-
value="${value//%/%%}"
|
|
482
|
-
value="${value//\\/\\\\}"
|
|
483
|
-
value="${value//\"/\\\"}"
|
|
484
|
-
value="${value//$'\n'/\\n}"
|
|
485
|
-
value="${value//$'\r'/\\r}"
|
|
486
|
-
value="${value//$'\t'/\\t}"
|
|
487
|
-
printf '"%s"' "$value"
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
write_service_unit() {
|
|
491
|
-
local id="$1" path service_path
|
|
492
|
-
path="$(unit_service_path "$id")"
|
|
493
|
-
service_path="$(unit_service_path_value)"
|
|
494
|
-
local runner_abs
|
|
495
|
-
runner_abs="$(cd "$(dirname "$RUNNER")" && pwd)/$(basename "$RUNNER")"
|
|
496
|
-
cat >"$path" <<EOF
|
|
497
|
-
# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.
|
|
498
|
-
[Unit]
|
|
499
|
-
Description=HQ Outpost scheduled job: ${id}
|
|
500
|
-
Documentation=file://${HQ_ROOT}/core/knowledge/public/hq-core/outpost-jobs-spec.md
|
|
501
|
-
After=network-online.target
|
|
502
|
-
Wants=network-online.target
|
|
503
|
-
|
|
504
|
-
[Service]
|
|
505
|
-
Type=oneshot
|
|
506
|
-
Environment=HQ_ROOT=${HQ_ROOT}
|
|
507
|
-
Environment=$(systemd_quote "PATH=$service_path")
|
|
508
|
-
ExecStart=${runner_abs} --hq-root ${HQ_ROOT} --job-id ${id}
|
|
509
|
-
Nice=10
|
|
510
|
-
|
|
511
|
-
[Install]
|
|
512
|
-
WantedBy=default.target
|
|
513
|
-
EOF
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
write_timer_unit() {
|
|
517
|
-
local id="$1" oncal="$2" path
|
|
518
|
-
path="$(unit_timer_path "$id")"
|
|
519
|
-
cat >"$path" <<EOF
|
|
520
|
-
# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.
|
|
521
|
-
[Unit]
|
|
522
|
-
Description=HQ Outpost scheduled job timer: ${id}
|
|
523
|
-
Documentation=file://${HQ_ROOT}/core/knowledge/public/hq-core/outpost-jobs-spec.md
|
|
524
|
-
|
|
525
|
-
[Timer]
|
|
526
|
-
OnCalendar=${oncal}
|
|
527
|
-
Persistent=true
|
|
528
|
-
RandomizedDelaySec=${RANDOMIZE_SEC}
|
|
529
|
-
Unit=hq-job-${id}.service
|
|
530
|
-
|
|
531
|
-
[Install]
|
|
532
|
-
WantedBy=timers.target
|
|
533
|
-
EOF
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
systemctl_user() {
|
|
537
|
-
if [ "$DRY_RUN" -eq 1 ]; then
|
|
538
|
-
log "dry-run systemctl --user $*"
|
|
539
|
-
return 0
|
|
540
|
-
fi
|
|
541
|
-
if [ "$SYSTEMCTL_BIN" = ":" ] || [ "$SYSTEMCTL_BIN" = "true" ]; then
|
|
542
|
-
return 0
|
|
543
|
-
fi
|
|
544
|
-
if ! command -v "$SYSTEMCTL_BIN" >/dev/null 2>&1; then
|
|
545
|
-
log "systemctl not available — units written but not enabled ($*)"
|
|
546
|
-
return 0
|
|
547
|
-
fi
|
|
548
|
-
"$SYSTEMCTL_BIN" --user "$@"
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
enable_timer() {
|
|
552
|
-
local id="$1"
|
|
553
|
-
local output rc=0
|
|
554
|
-
systemctl_user daemon-reload || true
|
|
555
|
-
output="$(systemctl_user enable --now "hq-job-${id}.timer" 2>&1)" || rc=$?
|
|
556
|
-
if [ "$rc" -ne 0 ]; then
|
|
557
|
-
log "systemctl enable timer failed for job $id exit=$rc: ${output:-no error message}"
|
|
558
|
-
return "$rc"
|
|
559
|
-
fi
|
|
560
|
-
if [ -n "$output" ]; then
|
|
561
|
-
printf '%s\n' "$output" >&2
|
|
562
|
-
fi
|
|
563
|
-
return 0
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
disable_remove_units() {
|
|
567
|
-
local id="$1"
|
|
568
|
-
systemctl_user disable --now "hq-job-${id}.timer" 2>/dev/null || true
|
|
569
|
-
systemctl_user stop "hq-job-${id}.service" 2>/dev/null || true
|
|
570
|
-
rm -f "$(unit_service_path "$id")" "$(unit_timer_path "$id")"
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
units_fingerprint() {
|
|
574
|
-
local id="$1"
|
|
575
|
-
local sp tp
|
|
576
|
-
sp="$(unit_service_path "$id")"
|
|
577
|
-
tp="$(unit_timer_path "$id")"
|
|
578
|
-
if [ ! -f "$sp" ] || [ ! -f "$tp" ]; then
|
|
579
|
-
printf ''
|
|
580
|
-
return
|
|
581
|
-
fi
|
|
582
|
-
# Content hash for idempotency compare
|
|
583
|
-
cat "$sp" "$tp" | shasum -a 256 2>/dev/null | awk '{print $1}' \
|
|
584
|
-
|| cat "$sp" "$tp" | cksum | awk '{print $1}'
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
# --- registry scan -----------------------------------------------------------
|
|
588
|
-
|
|
589
|
-
collect_job_files() {
|
|
590
|
-
{
|
|
591
|
-
# Only top-level registry YAMLs — skip sync conflict sidecars and quarantine dirs.
|
|
592
|
-
find "$HQ_ROOT/personal/jobs" -maxdepth 1 \( -name '*.yaml' -o -name '*.yml' \) -type f 2>/dev/null || true
|
|
593
|
-
find "$HQ_ROOT/companies" -path '*/jobs/*' -maxdepth 3 \( -name '*.yaml' -o -name '*.yml' \) -type f 2>/dev/null || true
|
|
594
|
-
} | grep -Ev '\.conflict-|/\.conflicts/' | sort -u
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
is_company_job_path() {
|
|
598
|
-
case "$1" in
|
|
599
|
-
*/companies/*/jobs/*) return 0 ;;
|
|
600
|
-
*) return 1 ;;
|
|
601
|
-
esac
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
ensure_sync_hook() {
|
|
605
|
-
local hook_dir="$HQ_ROOT/personal/hooks/Stop"
|
|
606
|
-
local hook_path="$hook_dir/90-outpost-jobs-reconcile.sh"
|
|
607
|
-
mkdir -p "$hook_dir"
|
|
608
|
-
cat >"$hook_path" <<EOF
|
|
609
|
-
#!/usr/bin/env bash
|
|
610
|
-
# Auto-installed by outpost-jobs-reconcile.sh --ensure-hook (US-004).
|
|
611
|
-
# hq-sync-runner executes personal/hooks/Stop/* after each sync cycle.
|
|
612
|
-
set -u
|
|
613
|
-
HQ_ROOT="\${OUTPOST_HQ_ROOT:-\${HQ_ROOT:-/home/ec2-user/hq}}"
|
|
614
|
-
RECONCILE="\$HQ_ROOT/core/scripts/outpost-jobs-reconcile.sh"
|
|
615
|
-
if [ -x "\$RECONCILE" ] || [ -f "\$RECONCILE" ]; then
|
|
616
|
-
bash "\$RECONCILE" --hq-root "\$HQ_ROOT" --after-sync >>"\${HOME}/.hq/jobs/reconcile/after-sync.log" 2>&1 || true
|
|
617
|
-
fi
|
|
618
|
-
EOF
|
|
619
|
-
chmod +x "$hook_path"
|
|
620
|
-
log "installed sync Stop hook: $hook_path"
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
# --- main reconcile ----------------------------------------------------------
|
|
624
|
-
|
|
625
|
-
BOX_OWNERS="$(resolve_box_owners)"
|
|
626
|
-
DESIRED_IDS=""
|
|
627
|
-
ERRORS=0
|
|
628
|
-
ARMED=0
|
|
629
|
-
SKIPPED=0
|
|
630
|
-
REMOVED=0
|
|
631
|
-
PROBE_RAN=0
|
|
632
|
-
|
|
633
|
-
if [ "$ENSURE_HOOK" -eq 1 ]; then
|
|
634
|
-
ensure_sync_hook
|
|
635
|
-
fi
|
|
636
|
-
|
|
637
|
-
# First pass: probe jobs that are not ready (unless --no-probe).
|
|
638
|
-
if [ "$NO_PROBE" -eq 0 ]; then
|
|
639
|
-
while IFS= read -r jf; do
|
|
640
|
-
[ -n "$jf" ] || continue
|
|
641
|
-
if ! bash "$VALIDATE" "$jf" >/dev/null 2>&1; then
|
|
642
|
-
continue
|
|
643
|
-
fi
|
|
644
|
-
jid="$(yq -r '.id // ""' "$jf")"
|
|
645
|
-
[ -n "$jid" ] || continue
|
|
646
|
-
if is_company_job_path "$jf"; then
|
|
647
|
-
jowner="$(yq -r '.owner // ""' "$jf")"
|
|
648
|
-
if ! owner_matches "$jowner" "$BOX_OWNERS"; then
|
|
649
|
-
continue
|
|
650
|
-
fi
|
|
651
|
-
fi
|
|
652
|
-
readiness="$(job_readiness "$jid")"
|
|
653
|
-
if [ "$readiness" != "ready" ]; then
|
|
654
|
-
log "probe $jid (cached readiness=$readiness)"
|
|
655
|
-
set +e
|
|
656
|
-
bash "$PROBE" --hq-root "$HQ_ROOT" "$jf" >/tmp/hq-job-probe-out.$$ 2>/tmp/hq-job-probe-err.$$
|
|
657
|
-
prc=$?
|
|
658
|
-
set -e
|
|
659
|
-
PROBE_RAN=$((PROBE_RAN + 1))
|
|
660
|
-
# Refresh cache from probe JSON stdout when present.
|
|
661
|
-
if [ -f /tmp/hq-job-probe-out.$$ ]; then
|
|
662
|
-
pready="$(jq -r '.readiness // empty' /tmp/hq-job-probe-out.$$ 2>/dev/null || true)"
|
|
663
|
-
if [ -n "$pready" ]; then
|
|
664
|
-
# Only cache definitive ready/blocked from a successful ingest path.
|
|
665
|
-
ingest="$(jq -r '.ingest // empty' /tmp/hq-job-probe-out.$$ 2>/dev/null || true)"
|
|
666
|
-
if [ "$ingest" = "ok" ] || [ "$ingest" = "dry_run" ]; then
|
|
667
|
-
write_status_cache "$jid" "$pready" "probe"
|
|
668
|
-
elif [ "$pready" = "blocked" ]; then
|
|
669
|
-
write_status_cache "$jid" "blocked" "probe-local"
|
|
670
|
-
fi
|
|
671
|
-
fi
|
|
672
|
-
fi
|
|
673
|
-
rm -f /tmp/hq-job-probe-out.$$ /tmp/hq-job-probe-err.$$
|
|
674
|
-
if [ "$prc" -ge 2 ] && [ "$prc" -ne 3 ]; then
|
|
675
|
-
ERRORS=$((ERRORS + 1))
|
|
676
|
-
fi
|
|
677
|
-
fi
|
|
678
|
-
done < <(collect_job_files)
|
|
679
|
-
fi
|
|
680
|
-
|
|
681
|
-
if [ "$PROBE_ONLY" -eq 1 ]; then
|
|
682
|
-
log "probe-only done (probed=$PROBE_RAN errors=$ERRORS)"
|
|
683
|
-
[ "$ERRORS" -eq 0 ] || exit 1
|
|
684
|
-
exit 0
|
|
685
|
-
fi
|
|
686
|
-
|
|
687
|
-
# Second pass: materialize / remove units.
|
|
688
|
-
while IFS= read -r jf; do
|
|
689
|
-
[ -n "$jf" ] || continue
|
|
690
|
-
|
|
691
|
-
job_id_hint="$(yq -r '.id // ""' "$jf" 2>/dev/null || true)"
|
|
692
|
-
tz="$(yq -r '.timezone // ""' "$jf" 2>/dev/null || true)"
|
|
693
|
-
if [ -n "$tz" ] && [[ ! "$tz" =~ ^[A-Za-z0-9/_+-]+$ ]]; then
|
|
694
|
-
log "skip ${job_id_hint:-unknown} — invalid timezone contains unsafe characters"
|
|
695
|
-
ERRORS=$((ERRORS + 1))
|
|
696
|
-
continue
|
|
697
|
-
fi
|
|
698
|
-
|
|
699
|
-
if ! bash "$VALIDATE" "$jf" >/dev/null 2>&1; then
|
|
700
|
-
log "skip invalid job YAML: $jf"
|
|
701
|
-
ERRORS=$((ERRORS + 1))
|
|
702
|
-
continue
|
|
703
|
-
fi
|
|
704
|
-
|
|
705
|
-
jid="$job_id_hint"
|
|
706
|
-
jname="$(yq -r '.name // ""' "$jf")"
|
|
707
|
-
jowner="$(yq -r '.owner // ""' "$jf")"
|
|
708
|
-
enabled="$(yq -o=json '.' "$jf" | jq -r 'if has("enabled") then (.enabled|tostring) else "true" end')"
|
|
709
|
-
schedule="$(yq -r '.schedule // ""' "$jf")"
|
|
710
|
-
if is_company_job_path "$jf"; then
|
|
711
|
-
if ! owner_matches "$jowner" "$BOX_OWNERS"; then
|
|
712
|
-
log "skip company job $jid — owner '$jowner' does not match this Outpost"
|
|
713
|
-
SKIPPED=$((SKIPPED + 1))
|
|
714
|
-
# Ensure we do not leave a stale timer if ownership changed.
|
|
715
|
-
if [ -f "$(unit_timer_path "$jid")" ]; then
|
|
716
|
-
disable_remove_units "$jid"
|
|
717
|
-
REMOVED=$((REMOVED + 1))
|
|
718
|
-
fi
|
|
719
|
-
continue
|
|
720
|
-
fi
|
|
721
|
-
fi
|
|
722
|
-
|
|
723
|
-
if [ "$enabled" = "false" ] || [ "$enabled" = "False" ] || [ "$enabled" = "0" ]; then
|
|
724
|
-
log "remove units for disabled job $jid"
|
|
725
|
-
if [ -f "$(unit_timer_path "$jid")" ] || [ -f "$(unit_service_path "$jid")" ]; then
|
|
726
|
-
disable_remove_units "$jid"
|
|
727
|
-
REMOVED=$((REMOVED + 1))
|
|
728
|
-
fi
|
|
729
|
-
continue
|
|
730
|
-
fi
|
|
731
|
-
|
|
732
|
-
readiness="$(job_readiness "$jid")"
|
|
733
|
-
if [ "$readiness" != "ready" ]; then
|
|
734
|
-
log "skip arming $jid — readiness=$readiness (need ready)"
|
|
735
|
-
SKIPPED=$((SKIPPED + 1))
|
|
736
|
-
if [ -f "$(unit_timer_path "$jid")" ]; then
|
|
737
|
-
disable_remove_units "$jid"
|
|
738
|
-
REMOVED=$((REMOVED + 1))
|
|
739
|
-
log "stopped timer for non-ready job $jid"
|
|
740
|
-
fi
|
|
741
|
-
continue
|
|
742
|
-
fi
|
|
743
|
-
|
|
744
|
-
# Parse 5-field cron
|
|
745
|
-
set -f
|
|
746
|
-
# shellcheck disable=SC2086
|
|
747
|
-
set -- $schedule
|
|
748
|
-
set +f
|
|
749
|
-
if [ "$#" -ne 5 ]; then
|
|
750
|
-
log "skip $jid — schedule not 5-field cron: $schedule"
|
|
751
|
-
ERRORS=$((ERRORS + 1))
|
|
752
|
-
continue
|
|
753
|
-
fi
|
|
754
|
-
oncal="$(cron_to_oncalendar "$1" "$2" "$3" "$4" "$5")" || {
|
|
755
|
-
log "skip $jid — cannot convert cron to OnCalendar: $schedule"
|
|
756
|
-
ERRORS=$((ERRORS + 1))
|
|
757
|
-
continue
|
|
758
|
-
}
|
|
759
|
-
if [ -n "$tz" ]; then
|
|
760
|
-
case " $oncal " in
|
|
761
|
-
*" $tz "*) ;;
|
|
762
|
-
*) oncal="$oncal $tz" ;;
|
|
763
|
-
esac
|
|
764
|
-
fi
|
|
765
|
-
|
|
766
|
-
DESIRED_IDS="${DESIRED_IDS:+${DESIRED_IDS}$'\n'}${jid}"
|
|
767
|
-
|
|
768
|
-
# Write to temp then compare for idempotency.
|
|
769
|
-
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/hq-job-unit.XXXXXX")"
|
|
770
|
-
OLD_UNIT_DIR="$UNIT_DIR"
|
|
771
|
-
UNIT_DIR="$tmp_dir"
|
|
772
|
-
write_service_unit "$jid"
|
|
773
|
-
write_timer_unit "$jid" "$oncal"
|
|
774
|
-
UNIT_DIR="$OLD_UNIT_DIR"
|
|
775
|
-
|
|
776
|
-
new_hash="$(cat "$tmp_dir/hq-job-${jid}.service" "$tmp_dir/hq-job-${jid}.timer" | shasum -a 256 2>/dev/null | awk '{print $1}' \
|
|
777
|
-
|| cat "$tmp_dir/hq-job-${jid}.service" "$tmp_dir/hq-job-${jid}.timer" | cksum | awk '{print $1}')"
|
|
778
|
-
old_hash="$(units_fingerprint "$jid")"
|
|
779
|
-
|
|
780
|
-
if [ "$new_hash" = "$old_hash" ] && [ -n "$old_hash" ]; then
|
|
781
|
-
log "noop $jid — units unchanged ($jname)"
|
|
782
|
-
rm -rf "$tmp_dir"
|
|
783
|
-
# Ensure timer still enabled
|
|
784
|
-
if enable_timer "$jid"; then
|
|
785
|
-
ARMED=$((ARMED + 1))
|
|
786
|
-
else
|
|
787
|
-
SKIPPED=$((SKIPPED + 1))
|
|
788
|
-
ERRORS=$((ERRORS + 1))
|
|
789
|
-
fi
|
|
790
|
-
continue
|
|
791
|
-
fi
|
|
792
|
-
|
|
793
|
-
cp "$tmp_dir/hq-job-${jid}.service" "$(unit_service_path "$jid")"
|
|
794
|
-
cp "$tmp_dir/hq-job-${jid}.timer" "$(unit_timer_path "$jid")"
|
|
795
|
-
rm -rf "$tmp_dir"
|
|
796
|
-
if enable_timer "$jid"; then
|
|
797
|
-
log "armed $jid OnCalendar=$oncal readiness=ready"
|
|
798
|
-
ARMED=$((ARMED + 1))
|
|
799
|
-
else
|
|
800
|
-
SKIPPED=$((SKIPPED + 1))
|
|
801
|
-
ERRORS=$((ERRORS + 1))
|
|
802
|
-
fi
|
|
803
|
-
done < <(collect_job_files)
|
|
804
|
-
|
|
805
|
-
# Remove units for jobs no longer in the desired set (deleted from registry).
|
|
806
|
-
for unit_file in "$UNIT_DIR"/hq-job-*.timer; do
|
|
807
|
-
[ -e "$unit_file" ] || continue
|
|
808
|
-
base="$(basename "$unit_file" .timer)"
|
|
809
|
-
uid="${base#hq-job-}"
|
|
810
|
-
found=0
|
|
811
|
-
while IFS= read -r d; do
|
|
812
|
-
[ "$d" = "$uid" ] && found=1 && break
|
|
813
|
-
done <<EOF
|
|
814
|
-
$DESIRED_IDS
|
|
815
|
-
EOF
|
|
816
|
-
if [ "$found" -eq 0 ]; then
|
|
817
|
-
log "remove orphaned units for deleted/unarmed job $uid"
|
|
818
|
-
disable_remove_units "$uid"
|
|
819
|
-
REMOVED=$((REMOVED + 1))
|
|
820
|
-
fi
|
|
821
|
-
done
|
|
822
|
-
|
|
823
|
-
SUMMARY="$(jq -nc \
|
|
824
|
-
--argjson armed "$ARMED" \
|
|
825
|
-
--argjson skipped "$SKIPPED" \
|
|
826
|
-
--argjson removed "$REMOVED" \
|
|
827
|
-
--argjson probed "$PROBE_RAN" \
|
|
828
|
-
--argjson errors "$ERRORS" \
|
|
829
|
-
--arg dry_run "$DRY_RUN" \
|
|
830
|
-
--arg at "$(iso_now)" \
|
|
831
|
-
'{armed:$armed,skipped:$skipped,removed:$removed,probed:$probed,errors:$errors,dry_run:($dry_run=="1"),at:$at}')"
|
|
832
|
-
printf '%s\n' "$SUMMARY" | tee "$STATE_DIR/last-reconcile.json" >/dev/null
|
|
833
|
-
log "done armed=$ARMED skipped=$SKIPPED removed=$REMOVED probed=$PROBE_RAN errors=$ERRORS"
|
|
834
|
-
|
|
835
|
-
[ "$ERRORS" -eq 0 ] || exit 1
|
|
836
|
-
exit 0
|