amsd-pipeline 1.9.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/bin/amsd-pipeline.js +20 -0
- package/install.sh +723 -0
- package/package.json +20 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// THIS FILE IS DELIBERATELY THIN. Every real decision — self-clone, packaging, docker isolation,
|
|
5
|
+
// uninstall — lives in install.sh, sourced from orchestrations-installer/install.sh at publish
|
|
6
|
+
// time (see ../sync-install-sh.sh) and never duplicated here. `npx amsd-pipeline` exists only to
|
|
7
|
+
// get a user from "no git commands" to "the same install.sh a colleague running from a checkout
|
|
8
|
+
// would run" with zero divergence between the two paths.
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
const installScript = path.join(__dirname, '..', 'install.sh');
|
|
13
|
+
const result = spawnSync('bash', [installScript, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
14
|
+
|
|
15
|
+
if (result.error) {
|
|
16
|
+
// bash itself could not be found/spawned — a clear message beats a raw ENOENT stack.
|
|
17
|
+
process.stderr.write(`amsd-pipeline: could not run bash (${result.error.message})\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/install.sh
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install.sh — set this machine up to run the pipeline.
|
|
3
|
+
#
|
|
4
|
+
# ./orchestrations-installer/install.sh install for the default stack, with dashboards if docker is up
|
|
5
|
+
# ./install.sh --stack codemie install for a specific stack
|
|
6
|
+
# ./install.sh --no-docker skip dashboards entirely
|
|
7
|
+
# ./install.sh --check verify an existing install, change nothing
|
|
8
|
+
# ./install.sh --dest ~/somewhere --ref v1.7 package that ref into a NEW tree, then install it
|
|
9
|
+
#
|
|
10
|
+
# --dest IS THE WHOLE POINT FOR ANYONE WHO IS NOT THIS CHECKOUT. Without it, install.sh only ever
|
|
11
|
+
# configures the tree it is already sitting inside — which presupposes the code already arrived by
|
|
12
|
+
# some means nobody ever automated. A colleague with repo access runs ONE command, from their own
|
|
13
|
+
# clone, naming a tagged commit on the shared remote: no manual git archive, no manual re-provision,
|
|
14
|
+
# no LLM standing in for either.
|
|
15
|
+
#
|
|
16
|
+
# DOCKER IS OPTIONAL, ALWAYS. The dashboards are observability; the pipeline runs without them.
|
|
17
|
+
# An installer that fails because a container is missing teaches people to skip the installer.
|
|
18
|
+
#
|
|
19
|
+
# The stacks, their runners and the services are all DECLARED (orchestrations/config/*). This
|
|
20
|
+
# script names none of them — adding a stack is a config edit, not an edit here.
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
# THE INSTALLER LIVES IN ITS OWN FOLDER; ROOT IS THE TREE IT INSTALLS.
|
|
24
|
+
#
|
|
25
|
+
# All 17 uses of $ROOT below address the install TREE — .env, dist/, orchestrations/, the compose
|
|
26
|
+
# file, the manifest. Resolving ROOT from this script's own directory would point every one of them
|
|
27
|
+
# inside orchestrations-installer/ and the install would silently configure the wrong tree.
|
|
28
|
+
#
|
|
29
|
+
# INSTALLER_DIR is separate and is only for this folder's own files, so the two can never be
|
|
30
|
+
# confused by a later edit.
|
|
31
|
+
INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
ROOT="$(cd "$INSTALLER_DIR/.." && pwd)"
|
|
33
|
+
CONFIG="$ROOT/orchestrations/config"
|
|
34
|
+
NODE_BIN="${NODE_BIN:-node}"
|
|
35
|
+
|
|
36
|
+
_ok() { printf '\033[0;32m ✓\033[0m %s\n' "$*"; }
|
|
37
|
+
_warn() { printf '\033[0;33m !\033[0m %s\n' "$*"; }
|
|
38
|
+
_bad() { printf '\033[0;31m ✗\033[0m %s\n' "$*" >&2; }
|
|
39
|
+
_head() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
|
40
|
+
|
|
41
|
+
STACK=""; USE_DOCKER=auto; CHECK_ONLY=0; FAILED=0
|
|
42
|
+
|
|
43
|
+
# THE CONTAINER RUNTIME IS DECLARED, NEVER INFERRED (plan §5.1a).
|
|
44
|
+
#
|
|
45
|
+
# Podman is the default on Windows because Docker Desktop needs a paid subscription above 250
|
|
46
|
+
# employees or $10M revenue. A procurement conversation, not a technical preference, is what stalls
|
|
47
|
+
# a rollout — and Podman on Windows runs on WSL2 too, so this is not a fourth platform.
|
|
48
|
+
CONTAINER_RUNTIME="${EPAM_CONTAINER_RUNTIME:-}"
|
|
49
|
+
|
|
50
|
+
# REPLAY IS A CONFIG OPTION (plan §5.1c). Langfuse is the RECORDER, not a dashboard: a run executed
|
|
51
|
+
# without it can never be replayed, because the turns were never captured. Off by default — 2.36GB
|
|
52
|
+
# of images is not a silent opt-in — but the consequence is one-way, so it is STATED either way.
|
|
53
|
+
REPLAY_MODE="${EPAM_REPLAY:-off}"
|
|
54
|
+
DEST=""; REF=""; REPO_URL=""; UNINSTALL=0
|
|
55
|
+
while [ $# -gt 0 ]; do
|
|
56
|
+
case "$1" in
|
|
57
|
+
--dest) DEST="${2:-}"; shift 2 ;;
|
|
58
|
+
--ref) REF="${2:-}"; shift 2 ;;
|
|
59
|
+
--repo) REPO_URL="${2:-}"; shift 2 ;;
|
|
60
|
+
--stack) STACK="${2:-}"; shift 2 ;;
|
|
61
|
+
--no-docker) USE_DOCKER=no; shift ;;
|
|
62
|
+
--docker) USE_DOCKER=yes; shift ;;
|
|
63
|
+
--check) CHECK_ONLY=1; shift ;;
|
|
64
|
+
--uninstall) UNINSTALL=1; shift ;;
|
|
65
|
+
--help|-h) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
66
|
+
*) _bad "unknown option '$1'"; exit 1 ;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
# ── Uninstall: the docker footprint ONLY, never the files on disk ────────────
|
|
71
|
+
#
|
|
72
|
+
# "we then have to be able to safely uninstall without affecting dev environment" (operator,
|
|
73
|
+
# 2026-09-03). STRUCTURALLY incapable of touching the dev environment, not merely unlikely to:
|
|
74
|
+
# isolated_project_name() always produces "test-install-amsd-pipeline-<suffix>-<number>"; the real
|
|
75
|
+
# hand-run dev stack names itself "dev-amsd-pipeline"/"dev-amsd-pipeline-launch" via each compose
|
|
76
|
+
# file's own top-level `name:` key. Those two prefixes cannot collide — verified live against real
|
|
77
|
+
# Docker, not assumed: the dev stack's actual volumes/networks/project name were confirmed
|
|
78
|
+
# completely distinct from an isolated install's.
|
|
79
|
+
#
|
|
80
|
+
# NEVER THE FILES. Run evidence and .env live under $ROOT (or --dest) either way — uninstall
|
|
81
|
+
# removes containers, the network, the volumes AND any images built for THIS install's own project
|
|
82
|
+
# (never a shared base image — `--rmi local` only removes images built by this compose file, not
|
|
83
|
+
# pulled ones), then prunes now-dangling build layers left behind by `--build`, and reports that
|
|
84
|
+
# the directory itself was left alone, in case an operator was about to `rm -rf` it on the
|
|
85
|
+
# assumption uninstall already did.
|
|
86
|
+
if [ "$UNINSTALL" = "1" ]; then
|
|
87
|
+
_head "Uninstall"
|
|
88
|
+
_UN_ROOT="$ROOT"
|
|
89
|
+
if [ -n "$DEST" ]; then
|
|
90
|
+
_UN_ROOT="$(cd "$DEST" 2>/dev/null && pwd)"
|
|
91
|
+
if [ -z "$_UN_ROOT" ]; then
|
|
92
|
+
_bad "--dest $DEST does not exist — nothing to uninstall there"
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
. "$INSTALLER_DIR/lib/isolated-compose-identity.sh"
|
|
97
|
+
if [ -f "$INSTALLER_DIR/lib/container-runtime.sh" ]; then
|
|
98
|
+
. "$INSTALLER_DIR/lib/container-runtime.sh"
|
|
99
|
+
else
|
|
100
|
+
_bad "missing $INSTALLER_DIR/lib/container-runtime.sh — cannot resolve a container runtime"
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
if ! CONTAINER_RUNTIME="$(container_runtime 2>/dev/null)"; then
|
|
104
|
+
_ok "no container runtime found — nothing docker-related to uninstall"
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
_UN_OBS_PROJECT="$(isolated_project_name "$_UN_ROOT" obs)"
|
|
108
|
+
_UN_LD_PROJECT="$(isolated_project_name "$_UN_ROOT" launch)"
|
|
109
|
+
for _UN_SPEC in "docker-compose.observability.yml:$_UN_OBS_PROJECT" "launch-dashboard/docker-compose.yml:$_UN_LD_PROJECT"; do
|
|
110
|
+
_UN_FILE="${_UN_SPEC%%:*}"; _UN_PROJECT="${_UN_SPEC##*:}"
|
|
111
|
+
_UN_COMPOSE="$_UN_ROOT/$_UN_FILE"
|
|
112
|
+
if [ -f "$_UN_COMPOSE" ]; then
|
|
113
|
+
if (cd "$(dirname "$_UN_COMPOSE")" && container_compose \
|
|
114
|
+
-f "$(basename "$_UN_COMPOSE")" -p "$_UN_PROJECT" down -v --remove-orphans --rmi local) >/dev/null 2>&1; then
|
|
115
|
+
_ok "removed $_UN_PROJECT (containers, network, volumes, images)"
|
|
116
|
+
else
|
|
117
|
+
_ok "$_UN_PROJECT: nothing to remove or already gone"
|
|
118
|
+
fi
|
|
119
|
+
# SCOPED PRUNE, never a bare `docker system prune` — that would also sweep up the dev
|
|
120
|
+
# environment's own dangling layers. Compose stamps every image it builds with this
|
|
121
|
+
# label, so filtering on it prunes only leftovers this exact project could have made.
|
|
122
|
+
"$CONTAINER_RUNTIME" image prune -f --filter "label=com.docker.compose.project=$_UN_PROJECT" >/dev/null 2>&1 || true
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
_ok "files under $_UN_ROOT were NOT touched — remove the directory yourself when you are done with it"
|
|
126
|
+
exit 0
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# ── Package a ref into a NEW tree, then install THAT — never a hand-run git archive ──
|
|
130
|
+
#
|
|
131
|
+
# THE SOURCE OF TRUTH IS THE GIT COMMIT, never a manual copy. `git archive` reads only tracked,
|
|
132
|
+
# committed content: untracked and gitignored files (real credentials among them) are structurally
|
|
133
|
+
# excluded, not filtered by a list that can miss one — proven this same repo: a raw tar shipped 8
|
|
134
|
+
# live credentials that git archive does not even see.
|
|
135
|
+
#
|
|
136
|
+
# INSTALLER_DIR NORMALLY NEVER MOVES. Every lib/*.sh this script sources still comes from where
|
|
137
|
+
# THIS install.sh lives, regardless of --dest — the archived copy that lands inside DEST is inert
|
|
138
|
+
# bystander content, same as it always has been. What --dest changes is $ROOT: everything below
|
|
139
|
+
# (.env, dist/, the compose files, the manifest) now addresses the freshly-packaged tree instead of
|
|
140
|
+
# wherever this script happened to be sitting.
|
|
141
|
+
#
|
|
142
|
+
# THE ONE EXCEPTION: a bare install.sh with no lib/ directory next to it (obtained alone — npx, a
|
|
143
|
+
# raw single-file download) self-clones below, and INSTALLER_DIR is repointed at that fresh clone's
|
|
144
|
+
# own orchestrations-installer/ — the only tree that is guaranteed to actually have the lib files
|
|
145
|
+
# this script is about to source.
|
|
146
|
+
if [ -n "$DEST" ]; then
|
|
147
|
+
_head "Packaging"
|
|
148
|
+
if git -C "$INSTALLER_DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
149
|
+
# THE REPO ROOT, NOT WHEREVER install.sh HAPPENS TO LIVE. `git archive` scopes its output
|
|
150
|
+
# to the CURRENT WORKING TREE'S SUBDIRECTORY, not the whole repo — running it with `-C
|
|
151
|
+
# $INSTALLER_DIR` (orchestrations-installer/, a SUBDIRECTORY) archived only install.sh and
|
|
152
|
+
# lib/, silently dropping the entire rest of the pipeline. Caught by actually running the
|
|
153
|
+
# packaged result and watching the very next step fail with "no provider-sets.json", not by
|
|
154
|
+
# reading the git-archive docs and assuming.
|
|
155
|
+
_GIT_ROOT="$(git -C "$INSTALLER_DIR" rev-parse --show-toplevel 2>/dev/null)"
|
|
156
|
+
if [ -z "$_GIT_ROOT" ]; then
|
|
157
|
+
_bad "could not resolve the repo root from $INSTALLER_DIR"
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
else
|
|
161
|
+
# NOTHING PRE-EXISTING REQUIRED. The only thing anyone needs to have obtained is
|
|
162
|
+
# install.sh ITSELF — however (npx, a raw download, a shared drive) — everything else,
|
|
163
|
+
# including the clone, happens here. This is what makes "for other people, not just me,
|
|
164
|
+
# and no local to use" actually true: someone with only this one file and git+node on PATH
|
|
165
|
+
# gets a full working install, no separate `git clone` step for them to run by hand.
|
|
166
|
+
#
|
|
167
|
+
# PUBLIC HTTPS, so no credential is needed to obtain it — the repo is public. --repo (or
|
|
168
|
+
# EPAM_REPO) overrides for a fork or a private mirror.
|
|
169
|
+
_REPO_URL="${REPO_URL:-${EPAM_REPO:-https://github.com/dune94/epam-cli.git}}"
|
|
170
|
+
_CLONE_DIR="$(mktemp -d)" || { _bad "could not create a temp directory to clone into"; exit 1; }
|
|
171
|
+
if ! git clone --quiet "$_REPO_URL" "$_CLONE_DIR" 2>&1; then
|
|
172
|
+
_bad "could not clone $_REPO_URL — check network access, or pass --repo for a different URL"
|
|
173
|
+
exit 1
|
|
174
|
+
fi
|
|
175
|
+
_ok "cloned $_REPO_URL"
|
|
176
|
+
_GIT_ROOT="$_CLONE_DIR"
|
|
177
|
+
# INSTALLER_DIR NOW MOVES — the one exception to the rule stated below. A bare install.sh
|
|
178
|
+
# obtained alone (npx, a raw single-file download) has no lib/ directory sitting next to
|
|
179
|
+
# it at all; every lib/*.sh source below would fail "No such file or directory" otherwise.
|
|
180
|
+
# The freshly-cloned tree has a complete, version-consistent copy of everything install.sh
|
|
181
|
+
# needs, so it becomes the new INSTALLER_DIR.
|
|
182
|
+
if [ -d "$_CLONE_DIR/orchestrations-installer/lib" ]; then
|
|
183
|
+
INSTALLER_DIR="$_CLONE_DIR/orchestrations-installer"
|
|
184
|
+
fi
|
|
185
|
+
fi
|
|
186
|
+
_PKG_REF="${REF:-HEAD}"
|
|
187
|
+
# FETCH FIRST: a colleague packaging a release just tagged by someone else may not have it yet.
|
|
188
|
+
git -C "$_GIT_ROOT" fetch --tags --quiet >/dev/null 2>&1 || true
|
|
189
|
+
if ! git -C "$_GIT_ROOT" rev-parse --verify "${_PKG_REF}^{commit}" >/dev/null 2>&1; then
|
|
190
|
+
_bad "ref '$_PKG_REF' does not exist in this checkout, even after fetching tags"
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
mkdir -p "$DEST" || { _bad "could not create $DEST"; exit 1; }
|
|
194
|
+
DEST="$(cd "$DEST" && pwd)"
|
|
195
|
+
# AN UPDATE MUST NEVER DESTROY RUN EVIDENCE. Extracting the ref straight over an EXISTING
|
|
196
|
+
# install would overwrite whatever that ref's git history holds at orchestrations/logs/ (5,268
|
|
197
|
+
# tracked files there, several genuinely real run evidence) — silently discarding a colleague's
|
|
198
|
+
# actual run history on every re-run. run-state-paths.json declares what an update must never
|
|
199
|
+
# touch; a first install into an empty $DEST is unaffected either way.
|
|
200
|
+
_RUN_STATE_EXCLUDES=()
|
|
201
|
+
if [ -f "$INSTALLER_DIR/run-state-paths.json" ]; then
|
|
202
|
+
. "$INSTALLER_DIR/lib/preserve-run-state.sh"
|
|
203
|
+
while IFS= read -r _excl; do
|
|
204
|
+
[ -n "$_excl" ] && _RUN_STATE_EXCLUDES+=("$_excl")
|
|
205
|
+
done < <(run_state_exclude_args "$INSTALLER_DIR/run-state-paths.json")
|
|
206
|
+
fi
|
|
207
|
+
# The ${arr[@]+"${arr[@]}"} form, not bare "${arr[@]}": bash <4.4 (macOS ships 3.2 by default,
|
|
208
|
+
# GPLv3 licensing) throws "unbound variable" under `set -u` expanding an empty array the plain
|
|
209
|
+
# way. This form is safe on every bash this installer might run under.
|
|
210
|
+
if ! git -C "$_GIT_ROOT" archive "$_PKG_REF" \
|
|
211
|
+
| tar -x -C "$DEST" "${_RUN_STATE_EXCLUDES[@]+"${_RUN_STATE_EXCLUDES[@]}"}"; then
|
|
212
|
+
_bad "packaging '$_PKG_REF' into $DEST failed"
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
_ok "packaged $_PKG_REF into $DEST"
|
|
216
|
+
ROOT="$DEST"
|
|
217
|
+
CONFIG="$ROOT/orchestrations/config"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# ── What stacks exist, and which is default? Read, never listed here. ────────
|
|
221
|
+
[ -f "$CONFIG/provider-sets.json" ] || { _bad "no provider-sets.json — is this the repo root?"; exit 1; }
|
|
222
|
+
read -r DEFAULT_SET ALL_SETS <<<"$("$NODE_BIN" -e '
|
|
223
|
+
const j = require(process.argv[1]);
|
|
224
|
+
process.stdout.write((j.defaultSet || "") + " " + Object.keys(j.sets || {}).join(","));
|
|
225
|
+
' "$CONFIG/provider-sets.json" 2>/dev/null)"
|
|
226
|
+
STACK="${STACK:-${EPAM_PROVIDER_SET:-$DEFAULT_SET}}"
|
|
227
|
+
case ",$ALL_SETS," in
|
|
228
|
+
*",$STACK,"*) : ;;
|
|
229
|
+
*) _bad "unknown stack '$STACK' — declared stacks are: ${ALL_SETS//,/, }"; exit 1 ;;
|
|
230
|
+
esac
|
|
231
|
+
|
|
232
|
+
_head "Stack: $STACK (available: ${ALL_SETS//,/, })"
|
|
233
|
+
|
|
234
|
+
# ── Prerequisites the run genuinely needs ───────────────────────────────────
|
|
235
|
+
_head "Prerequisites"
|
|
236
|
+
need() {
|
|
237
|
+
local cmd="$1" why="$2"
|
|
238
|
+
if command -v "$cmd" >/dev/null 2>&1; then _ok "$cmd"
|
|
239
|
+
else _bad "$cmd is missing — $why"; FAILED=1; fi
|
|
240
|
+
}
|
|
241
|
+
need git "the pipeline works on git codelines"
|
|
242
|
+
need jq "the pipeline parses JSON with it throughout"
|
|
243
|
+
need node "the engine and the CLI are node"
|
|
244
|
+
|
|
245
|
+
# The runner this stack declares. This is the commonest real failure.
|
|
246
|
+
RUNNER="$("$NODE_BIN" -e '
|
|
247
|
+
const fs = require("fs"), path = require("path");
|
|
248
|
+
const reg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
249
|
+
const set = reg.sets[process.argv[2]];
|
|
250
|
+
const s = JSON.parse(fs.readFileSync(path.join(path.dirname(process.argv[1]), set.settingsFile), "utf8"));
|
|
251
|
+
process.stdout.write(Object.keys(s.runners || {})[0] || "");
|
|
252
|
+
' "$CONFIG/provider-sets.json" "$STACK" 2>/dev/null)"
|
|
253
|
+
if [ -n "$RUNNER" ]; then
|
|
254
|
+
if command -v "$RUNNER" >/dev/null 2>&1; then _ok "$RUNNER (the '$STACK' runner)"
|
|
255
|
+
else _bad "'$RUNNER' is not on PATH — the '$STACK' stack cannot run without it"; FAILED=1; fi
|
|
256
|
+
|
|
257
|
+
# PYTHON IS A RUNTIME DEPENDENCY, not an optional extra: 88 handlers under
|
|
258
|
+
# orchestrations/scripts/lib/handlers are executed with it.
|
|
259
|
+
#
|
|
260
|
+
# Measured, because the assumption was bigger than the truth: every import across all 88 is
|
|
261
|
+
# stdlib, plus one LOCAL module (_testfile, imported by siblings in the same directory). So there
|
|
262
|
+
# is no venv to provision, no pip install, no requirements.txt — the interpreter is the whole
|
|
263
|
+
# requirement, and checking for it is the whole job.
|
|
264
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
265
|
+
_ok "python3 ($(python3 -V 2>&1 | awk '{print $2}')) — 88 handlers need it"
|
|
266
|
+
else
|
|
267
|
+
_bad "python3 is not on PATH — 88 pipeline handlers cannot run without it"; FAILED=1
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# ── Credentials: what this stack needs, from what it declares ───────────────
|
|
272
|
+
_head "Credentials"
|
|
273
|
+
# .env.example IS GENERATED, never hand-maintained — it went stale in both directions: this repo's
|
|
274
|
+
# root template never mentioned openrouter's own required OPENROUTER_API_KEY/MINIMAX_API_KEY
|
|
275
|
+
# (declared in provider-sets.json, absent from what an operator was told to fill in), and
|
|
276
|
+
# launch-dashboard's template kept calling EPAM_PROVIDER_SET "REQUIRED" long after that requirement
|
|
277
|
+
# was removed from config.js. Regenerated every install so it cannot drift stale again — .env
|
|
278
|
+
# itself is never touched, only the template.
|
|
279
|
+
if [ "$CHECK_ONLY" = "0" ] && [ -f "$CONFIG/provider-sets.json" ]; then
|
|
280
|
+
. "$INSTALLER_DIR/lib/generate-env-example.sh"
|
|
281
|
+
if generate_env_example "$CONFIG/provider-sets.json" "$CONFIG/env-vars.json" "$ROOT/.env.example" 2>/dev/null; then
|
|
282
|
+
_ok ".env.example regenerated from provider-sets.json"
|
|
283
|
+
else
|
|
284
|
+
_warn "could not regenerate .env.example — using whatever is already there"
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
if [ -f "$ROOT/.env" ]; then
|
|
289
|
+
_ok ".env present"
|
|
290
|
+
else
|
|
291
|
+
# THE TEMPLATE IS NAMED .env.example. This read .env.sample — a file that does not exist and
|
|
292
|
+
# never has — guarded by `[ -f ]`, so the guard was false, the copy never happened, and NOTHING
|
|
293
|
+
# WAS SAID. The operator was then told to fill in a file the installer had not created.
|
|
294
|
+
#
|
|
295
|
+
# Resolved from a declared list so a rename cannot silently reintroduce the same no-op, and an
|
|
296
|
+
# ABSENT template is reported by name rather than passed over.
|
|
297
|
+
_tpl=""
|
|
298
|
+
for _c in "$ROOT/.env.example" "$ROOT/.env.sample"; do
|
|
299
|
+
[ -f "$_c" ] && { _tpl="$_c"; break; }
|
|
300
|
+
done
|
|
301
|
+
if [ "$CHECK_ONLY" = "1" ]; then
|
|
302
|
+
_bad ".env is missing"; FAILED=1
|
|
303
|
+
elif [ -n "$_tpl" ]; then
|
|
304
|
+
cp "$_tpl" "$ROOT/.env"
|
|
305
|
+
_warn ".env created from $(basename "$_tpl") — FILL IT IN before running"
|
|
306
|
+
else
|
|
307
|
+
_bad "no .env and no template (.env.example) to create one from"; FAILED=1
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# EXISTENCE IS NOT SUFFICIENCY — the same defect class already fixed for dist/epam.js
|
|
312
|
+
# ("EXISTENCE IS NOT A BUILD, either way"). A copied-but-unfilled .env (still holding the empty
|
|
313
|
+
# placeholders the template ships with) reported "present" and nothing more, so a stack's own
|
|
314
|
+
# REQUIRED credential could sit empty all the way to the first paid call before anyone noticed.
|
|
315
|
+
if [ -f "$ROOT/.env" ] && [ -f "$ROOT/orchestrations/scripts/lib/set-credentials.sh" ]; then
|
|
316
|
+
_missing_creds="$(
|
|
317
|
+
set -a; . "$ROOT/.env" 2>/dev/null; set +a
|
|
318
|
+
. "$ROOT/orchestrations/scripts/lib/set-credentials.sh"
|
|
319
|
+
while IFS=$'\t' read -r _c_env _c_from _c_req; do
|
|
320
|
+
[ "$_c_req" = "1" ] || continue
|
|
321
|
+
eval "_c_val=\${$_c_from:-}"
|
|
322
|
+
[ -z "$_c_val" ] && printf '%s ' "$_c_from"
|
|
323
|
+
done < <(EPAM_PROVIDER_SET="$STACK" _set_credentials_decl 2>/dev/null)
|
|
324
|
+
)"
|
|
325
|
+
if [ -n "$(printf '%s' "$_missing_creds" | tr -d '[:space:]')" ]; then
|
|
326
|
+
_bad "the '$STACK' stack needs these, still empty in .env: $_missing_creds"
|
|
327
|
+
FAILED=1
|
|
328
|
+
else
|
|
329
|
+
_ok "required '$STACK' credentials are filled in"
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# ── Build ───────────────────────────────────────────────────────────────────
|
|
334
|
+
_head "Build"
|
|
335
|
+
# A PACKAGED INSTALL HAS NO src/. That is the artefact this installer exists to install: dist/ and
|
|
336
|
+
# orchestrations/ without the CLI source (§1.4 of the packaging plan). `npm run build` runs tsup,
|
|
337
|
+
# which needs src/ AND the dev dependencies — a client tree has neither, so building
|
|
338
|
+
# unconditionally fails at the one step that cannot be skipped.
|
|
339
|
+
#
|
|
340
|
+
# The rule: build when there is source to build FROM, verify otherwise, and SAY WHICH HAPPENED.
|
|
341
|
+
# Never silently skip; never fail on a tree that is already complete.
|
|
342
|
+
#
|
|
343
|
+
# EXISTENCE IS NOT A BUILD, either way. `[ -f dist/epam.js ]` passes on a 188-byte stub, so the old
|
|
344
|
+
# check reported success for a tree that could not run. The threshold sits far below any real
|
|
345
|
+
# bundle and far above any stub.
|
|
346
|
+
_dist="$ROOT/dist/epam.js"
|
|
347
|
+
_min_bytes="${EPAM_MIN_DIST_BYTES:-51200}"
|
|
348
|
+
|
|
349
|
+
# WHETHER dist/epam.js MATTERS DEPENDS ON THE STACK, and the stack declares it.
|
|
350
|
+
#
|
|
351
|
+
# claude.sh:1649-1650 routes copilot|openai|openrouter|cursor|minimax|epam to $EPAM_CLI
|
|
352
|
+
# (dist/epam.js), and `claude` to $CLAUDE_CMD. Every provider set declares `claude` or
|
|
353
|
+
# `codemie-claude` as its runner, so on those stacks dist/epam.js is NEVER executed — which is why
|
|
354
|
+
# this repo runs green with a 188-byte "Hello, World!" stub in dist/.
|
|
355
|
+
#
|
|
356
|
+
# So a hard failure here would refuse an install that works. It is reported instead, with what it
|
|
357
|
+
# means, and only FAILS when the stack actually routes to the epam CLI. The check that matters for
|
|
358
|
+
# every stack — is the declared RUNNER on PATH — already runs above.
|
|
359
|
+
_verify_dist() {
|
|
360
|
+
local _needs_epam=0
|
|
361
|
+
case "$RUNNER" in
|
|
362
|
+
epam|"") _needs_epam=1 ;;
|
|
363
|
+
esac
|
|
364
|
+
|
|
365
|
+
if [ ! -f "$_dist" ]; then
|
|
366
|
+
if [ "$_needs_epam" = "1" ]; then
|
|
367
|
+
_bad "dist/epam.js is missing and the '$STACK' stack needs it"; FAILED=1; return 1
|
|
368
|
+
fi
|
|
369
|
+
_warn "dist/epam.js is absent — not needed by the '$STACK' stack (runner: $RUNNER)"
|
|
370
|
+
return 0
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
_size=$(wc -c < "$_dist" 2>/dev/null | tr -d ' ')
|
|
374
|
+
if [ "${_size:-0}" -lt "$_min_bytes" ]; then
|
|
375
|
+
if [ "$_needs_epam" = "1" ]; then
|
|
376
|
+
_bad "dist/epam.js is only ${_size} bytes — that is a stub, not a build, and the '$STACK' stack needs it"
|
|
377
|
+
FAILED=1; return 1
|
|
378
|
+
fi
|
|
379
|
+
_warn "dist/epam.js is a ${_size}-byte stub — harmless for the '$STACK' stack (runner: $RUNNER), but it is not a build"
|
|
380
|
+
return 0
|
|
381
|
+
fi
|
|
382
|
+
_ok "dist/epam.js present (${_size} bytes)"
|
|
383
|
+
return 0
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if [ ! -d "$ROOT/src" ]; then
|
|
387
|
+
# The packaged case. Stated explicitly so nobody reads "ok" and assumes a build happened here.
|
|
388
|
+
_ok "packaged install — no src/, using the shipped pre-built bundle"
|
|
389
|
+
_verify_dist
|
|
390
|
+
elif [ "$CHECK_ONLY" = "1" ]; then
|
|
391
|
+
_verify_dist
|
|
392
|
+
else
|
|
393
|
+
if [ ! -d "$ROOT/node_modules" ]; then
|
|
394
|
+
_warn "installing dependencies (this takes a minute)"
|
|
395
|
+
(cd "$ROOT" && npm install --silent) || { _bad "npm install failed"; FAILED=1; }
|
|
396
|
+
else _ok "node_modules present"; fi
|
|
397
|
+
if (cd "$ROOT" && npm run build --silent >/dev/null 2>&1); then
|
|
398
|
+
_verify_dist
|
|
399
|
+
else
|
|
400
|
+
_bad "build failed — run 'npm run build' to see why"; FAILED=1
|
|
401
|
+
fi
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
# ── The `epam` shim ───────────────────────────────────────────────────────────
|
|
405
|
+
# THE SHIM MUST POINT AT THIS INSTALL. The one found on the development machine reads
|
|
406
|
+
# exec node /home/<someone>/projects/ai/epam-cli/dist/epam.js "$@"
|
|
407
|
+
# which is correct for exactly one checkout and wrong for every install. The path is computed here,
|
|
408
|
+
# at install time, from where this script actually is.
|
|
409
|
+
_head "Command"
|
|
410
|
+
BIN_DIR="${EPAM_BIN_DIR:-$HOME/.local/bin}"
|
|
411
|
+
if [ "$CHECK_ONLY" = "1" ]; then
|
|
412
|
+
if [ -x "$BIN_DIR/epam" ]; then _ok "epam shim present at $BIN_DIR/epam"
|
|
413
|
+
else _warn "no epam shim at $BIN_DIR/epam"; fi
|
|
414
|
+
else
|
|
415
|
+
mkdir -p "$BIN_DIR"
|
|
416
|
+
printf '#!/usr/bin/env bash\nexec node "%s/dist/epam.js" "$@"\n' "$ROOT" > "$BIN_DIR/epam"
|
|
417
|
+
chmod +x "$BIN_DIR/epam"
|
|
418
|
+
_ok "epam shim written to $BIN_DIR/epam -> $ROOT/dist/epam.js"
|
|
419
|
+
case ":$PATH:" in
|
|
420
|
+
*":$BIN_DIR:"*) : ;;
|
|
421
|
+
# Said, never done silently: editing a shell profile behind an operator is a surprise, and
|
|
422
|
+
# a shim that is not on PATH is a shim that does nothing.
|
|
423
|
+
*) _warn "$BIN_DIR is not on PATH — add it: export PATH=\"$BIN_DIR:\$PATH\"" ;;
|
|
424
|
+
esac
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
# ── Project config: the three things that cannot be derived ───────────────────
|
|
428
|
+
# JIRA_URL, JIRA_PROJECT_KEY and JIRA_CODELINE_ROOT are answers, not defaults — nothing in the tree
|
|
429
|
+
# can infer which Jira site, which project, or where the codelines live.
|
|
430
|
+
#
|
|
431
|
+
# NON-SECRET VALUES ONLY. Credentials live in one .env, owned by the operator. This never prompts
|
|
432
|
+
# for a token, never echoes one, and never writes one into project config: one file, one owner, one
|
|
433
|
+
# place to look.
|
|
434
|
+
_head "Project"
|
|
435
|
+
EPAM_PROJECT="${EPAM_PROJECT:-}"
|
|
436
|
+
if [ -n "$EPAM_PROJECT" ] && [ -d "$ROOT/orchestrations/projects/$EPAM_PROJECT" ]; then
|
|
437
|
+
_cfg="$ROOT/orchestrations/projects/$EPAM_PROJECT/config.env"
|
|
438
|
+
if [ "$CHECK_ONLY" = "1" ]; then
|
|
439
|
+
[ -f "$_cfg" ] && _ok "config.env present for '$EPAM_PROJECT'" || _warn "no config.env for '$EPAM_PROJECT'"
|
|
440
|
+
else
|
|
441
|
+
_missing=""
|
|
442
|
+
for _v in JIRA_URL JIRA_PROJECT_KEY JIRA_CODELINE_ROOT; do
|
|
443
|
+
eval "_val=\${$_v:-}"
|
|
444
|
+
[ -z "$_val" ] && _missing="$_missing $_v"
|
|
445
|
+
done
|
|
446
|
+
if [ -n "$_missing" ]; then
|
|
447
|
+
_warn "project '$EPAM_PROJECT' not configured — missing:$_missing"
|
|
448
|
+
else
|
|
449
|
+
{
|
|
450
|
+
echo "# Written by install.sh. NON-SECRET VALUES ONLY — credentials live in .env."
|
|
451
|
+
echo "PROJECT_NAME=$EPAM_PROJECT"
|
|
452
|
+
echo "JIRA_URL=$JIRA_URL"
|
|
453
|
+
echo "JIRA_PROJECT_KEY=$JIRA_PROJECT_KEY"
|
|
454
|
+
echo "JIRA_CODELINE_ROOT=$JIRA_CODELINE_ROOT"
|
|
455
|
+
} > "$_cfg"
|
|
456
|
+
_ok "wrote $_cfg (JIRA_URL, JIRA_PROJECT_KEY, JIRA_CODELINE_ROOT)"
|
|
457
|
+
fi
|
|
458
|
+
fi
|
|
459
|
+
else
|
|
460
|
+
_ok "no project selected (set EPAM_PROJECT to configure one)"
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
# ── Container runtime ─────────────────────────────────────────────────────────
|
|
464
|
+
_head "Container runtime"
|
|
465
|
+
# ONE RESOLVER, ASKED — not a third copy of the rule. The installer, dashboard-health-check.sh and
|
|
466
|
+
# pre-run-reset.sh each had their own idea of which runtime to use, and only this one had ever
|
|
467
|
+
# heard of podman.
|
|
468
|
+
_CR_LIB="$INSTALLER_DIR/lib/container-runtime.sh"
|
|
469
|
+
if [ -f "$_CR_LIB" ]; then
|
|
470
|
+
# shellcheck source=orchestrations-installer/lib/container-runtime.sh
|
|
471
|
+
. "$_CR_LIB"
|
|
472
|
+
else
|
|
473
|
+
_bad "missing $_CR_LIB — this tree cannot resolve a container runtime"; FAILED=1
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
_CR_DECLARED="$CONTAINER_RUNTIME"
|
|
477
|
+
if CONTAINER_RUNTIME="$(container_runtime 2>&1)"; then
|
|
478
|
+
if [ -n "$_CR_DECLARED" ]; then _ok "runtime: $CONTAINER_RUNTIME (declared)"
|
|
479
|
+
else _ok "runtime: $CONTAINER_RUNTIME (discovered)"
|
|
480
|
+
fi
|
|
481
|
+
else
|
|
482
|
+
# The resolver's own message says WHICH runtimes it looked for, so it is reported verbatim
|
|
483
|
+
# rather than restated here in words that could drift from the declaration.
|
|
484
|
+
_CR_WHY="$CONTAINER_RUNTIME"
|
|
485
|
+
CONTAINER_RUNTIME=none
|
|
486
|
+
if [ -n "$_CR_DECLARED" ]; then
|
|
487
|
+
# A runtime the installer cannot drive must fail HERE, not surface later as a compose
|
|
488
|
+
# command that does nothing.
|
|
489
|
+
_bad "$_CR_WHY"; FAILED=1
|
|
490
|
+
else
|
|
491
|
+
# Nothing installed is not a failure: --no-docker is a supported install.
|
|
492
|
+
_ok "runtime: none — no container runtime found, the pipeline still runs"
|
|
493
|
+
fi
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
# ── Replay ────────────────────────────────────────────────────────────────────
|
|
497
|
+
_head "Replay"
|
|
498
|
+
case "$REPLAY_MODE" in
|
|
499
|
+
off)
|
|
500
|
+
# The cost is one-way and must be stated: nothing recorded now can be replayed later.
|
|
501
|
+
_ok "replay: off — runs will NOT be replayable (no Langfuse recorder installed)" ;;
|
|
502
|
+
on)
|
|
503
|
+
_ok "replay: on — Langfuse records every run so it can be replayed for \$0"
|
|
504
|
+
# LangfuseTracer.ts:30 gates on BOTH keys. A fresh install has empty volumes, so no project
|
|
505
|
+
# and no keys exist — and recording is silently off while the containers run and capture
|
|
506
|
+
# nothing. That is the one case where a warning is not enough.
|
|
507
|
+
_lf_missing=""
|
|
508
|
+
[ -z "${LANGFUSE_SECRET_KEY:-}" ] && _lf_missing="$_lf_missing LANGFUSE_SECRET_KEY"
|
|
509
|
+
[ -z "${LANGFUSE_PUBLIC_KEY:-}" ] && _lf_missing="$_lf_missing LANGFUSE_PUBLIC_KEY"
|
|
510
|
+
if [ -n "$_lf_missing" ]; then
|
|
511
|
+
_bad "replay: on but missing:$_lf_missing — nothing would be recorded, and a run not recorded can never be replayed"
|
|
512
|
+
FAILED=1
|
|
513
|
+
else
|
|
514
|
+
_ok "Langfuse keys present — recording is active" ;
|
|
515
|
+
fi ;;
|
|
516
|
+
*)
|
|
517
|
+
_bad "unknown replay mode '$REPLAY_MODE' — expected on or off"; FAILED=1 ;;
|
|
518
|
+
esac
|
|
519
|
+
|
|
520
|
+
# ── Dashboards: OPTIONAL, and never a reason to fail ────────────────────────
|
|
521
|
+
_head "Dashboards (optional)"
|
|
522
|
+
# THE PROBE ASKS THE RESOLVED RUNTIME. It said `docker` literally, so on a podman-only machine the
|
|
523
|
+
# installer announced "runtime: podman" and then started nothing — the report and the behaviour
|
|
524
|
+
# disagreeing, which is this file's recurring defect.
|
|
525
|
+
runtime_up() {
|
|
526
|
+
[ "$CONTAINER_RUNTIME" = "none" ] && return 1
|
|
527
|
+
command -v "$CONTAINER_RUNTIME" >/dev/null 2>&1 && "$CONTAINER_RUNTIME" info >/dev/null 2>&1
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# THE COMPOSE FILE IS NAMED. This ran `docker compose up -d` with no -f, and there is no
|
|
531
|
+
# docker-compose.yml at the repo root — only the named files below. It ended in `|| true`, so the
|
|
532
|
+
# failure was swallowed and the installer reported "docker is up" having started nothing.
|
|
533
|
+
COMPOSE_FILE="${EPAM_COMPOSE_FILE:-$ROOT/docker-compose.observability.yml}"
|
|
534
|
+
# ISOLATED FROM EVERY OTHER INSTALL ON THIS MACHINE, DETERMINISTICALLY — never a human hand-picking
|
|
535
|
+
# a free subnet. The compose file's own default (EPAM_OBS_SUBNET:-172.31.0.0/16) is a FIXED
|
|
536
|
+
# constant: two installs, or this exact install colliding with an already-running dev checkout on
|
|
537
|
+
# that same default, hit "Pool overlaps with other one on this address space" — the launch-dashboard
|
|
538
|
+
# section already had this fix; the observability stack never did.
|
|
539
|
+
. "$INSTALLER_DIR/lib/isolated-compose-identity.sh"
|
|
540
|
+
_OBS_PROJECT="$(isolated_project_name "$ROOT" obs)"
|
|
541
|
+
compose_up() {
|
|
542
|
+
if [ ! -f "$COMPOSE_FILE" ]; then
|
|
543
|
+
_bad "compose file not found: $COMPOSE_FILE"; FAILED=1; return 1
|
|
544
|
+
fi
|
|
545
|
+
local _up=1 _log _subnet _i=0
|
|
546
|
+
_log="$(mktemp)"
|
|
547
|
+
for _subnet in $(isolated_subnet_candidates "$ROOT"); do
|
|
548
|
+
# ATTEMPT 0 KEEPS THE WELL-KNOWN PORTS EXACTLY (offset 0) — a normal single-install machine
|
|
549
|
+
# sees no change at all, still :3100, :8092, :8123, :3001. Only a genuine collision (this
|
|
550
|
+
# exact stack already running, or a second install) steps to the next attempt's offset, so
|
|
551
|
+
# no manual port flag is ever required for this to just work.
|
|
552
|
+
local _off=$((_i * 10))
|
|
553
|
+
if (cd "$ROOT" && EPAM_OBS_SUBNET="$_subnet" \
|
|
554
|
+
EPAM_OBS_CLICKHOUSE_PORT=$((8123 + _off)) \
|
|
555
|
+
EPAM_OBS_LANGFUSE_PORT=$((3100 + _off)) \
|
|
556
|
+
EPAM_OBS_DASHBOARD_PORT=$((8092 + _off)) \
|
|
557
|
+
EPAM_OBS_GRAFANA_PORT=$((3001 + _off)) \
|
|
558
|
+
container_compose -f "$COMPOSE_FILE" -p "$_OBS_PROJECT" up -d) >"$_log" 2>&1; then
|
|
559
|
+
_up=0
|
|
560
|
+
break
|
|
561
|
+
fi
|
|
562
|
+
# ONLY RETRY ON A SUBNET OR PORT COLLISION — any other failure would fail identically on
|
|
563
|
+
# every candidate, burning through all of them and hiding the real error behind repeats.
|
|
564
|
+
grep -qiE 'overlap|pool|port is already allocated|address already in use' "$_log" || break
|
|
565
|
+
# TEAR DOWN BEFORE THE NEXT ATTEMPT. A failed `up -d` still CREATES whatever services it
|
|
566
|
+
# got to before the failing one — found live: postgres/redis/clickhouse started fine on
|
|
567
|
+
# attempt 0, agent-monitor's port collision failed the overall command, and attempt 1's
|
|
568
|
+
# NEW port env was silently ignored for the already-Created containers, which stayed bound
|
|
569
|
+
# to attempt 0's (colliding) ports. Compose does not cleanly re-resolve an already-created
|
|
570
|
+
# container's config from new env vars on a later `up` — each attempt needs a clean slate.
|
|
571
|
+
(cd "$ROOT" && container_compose -f "$COMPOSE_FILE" -p "$_OBS_PROJECT" down) >/dev/null 2>&1 || true
|
|
572
|
+
_i=$((_i + 1))
|
|
573
|
+
done
|
|
574
|
+
if [ "$_up" != "0" ]; then
|
|
575
|
+
_bad "$CONTAINER_RUNTIME compose failed for $COMPOSE_FILE — the services are NOT running: $(tail -3 "$_log" 2>/dev/null)"
|
|
576
|
+
FAILED=1
|
|
577
|
+
rm -f "$_log" 2>/dev/null
|
|
578
|
+
return 1
|
|
579
|
+
fi
|
|
580
|
+
rm -f "$_log" 2>/dev/null
|
|
581
|
+
return 0
|
|
582
|
+
}
|
|
583
|
+
case "$USE_DOCKER" in
|
|
584
|
+
no) _ok "skipped (--no-docker) — the pipeline runs without them" ;;
|
|
585
|
+
yes) if runtime_up; then
|
|
586
|
+
if [ "$CHECK_ONLY" = "1" ] || compose_up; then _ok "$CONTAINER_RUNTIME is up — services started"; fi
|
|
587
|
+
else _bad "--docker was requested but no container runtime is running (resolved: $CONTAINER_RUNTIME)"; FAILED=1; fi ;;
|
|
588
|
+
auto) if runtime_up; then
|
|
589
|
+
if [ "$CHECK_ONLY" = "1" ] || compose_up; then _ok "$CONTAINER_RUNTIME is up — dashboards available"; fi
|
|
590
|
+
else _warn "no container runtime is running — dashboards unavailable, THE PIPELINE STILL RUNS"; fi ;;
|
|
591
|
+
esac
|
|
592
|
+
|
|
593
|
+
# ── Launch dashboard: OPTIONAL, and must be genuinely UP when it claims to be ─
|
|
594
|
+
# THIS CANNOT DEPEND ON A HUMAN OR AN LLM DOING IT BY HAND. A rebuild-and-restart done manually
|
|
595
|
+
# once is a rebuild-and-restart that must be done manually every time — this makes it the same
|
|
596
|
+
# re-run of install.sh as everything else above.
|
|
597
|
+
#
|
|
598
|
+
# `docker compose up -d` alone does NOT rebuild an image from changed source; it recreates
|
|
599
|
+
# containers from whatever image already exists. --build closes that. And `up -d` exiting 0 means
|
|
600
|
+
# containers were CREATED, not that the service inside is ready to answer a request — closed by
|
|
601
|
+
# actually polling the health endpoint below rather than trusting the exit code.
|
|
602
|
+
_head "Launch dashboard (optional)"
|
|
603
|
+
LAUNCH_DIR="$ROOT/launch-dashboard"
|
|
604
|
+
LAUNCH_COMPOSE="$LAUNCH_DIR/docker-compose.yml"
|
|
605
|
+
LAUNCH_HEALTH_TRIES="${EPAM_LAUNCH_HEALTH_TRIES:-30}"
|
|
606
|
+
LAUNCH_HEALTH_INTERVAL="${EPAM_LAUNCH_HEALTH_INTERVAL:-1}"
|
|
607
|
+
LAUNCH_STATUS=absent
|
|
608
|
+
|
|
609
|
+
if [ ! -f "$LAUNCH_COMPOSE" ]; then
|
|
610
|
+
_ok "not present in this tree — nothing to provision"
|
|
611
|
+
elif [ "$USE_DOCKER" = "no" ]; then
|
|
612
|
+
LAUNCH_STATUS=skipped
|
|
613
|
+
_ok "skipped (--no-docker)"
|
|
614
|
+
else
|
|
615
|
+
. "$INSTALLER_DIR/lib/wait-for-health.sh"
|
|
616
|
+
. "$INSTALLER_DIR/lib/isolated-compose-identity.sh"
|
|
617
|
+
|
|
618
|
+
# A REAL PASSWORD IS A DECISION ONLY A HUMAN MAKES — never synthesized here. Mirrors the root
|
|
619
|
+
# .env handling: copy the template so there is something to fill in, never invent a secret.
|
|
620
|
+
if [ ! -f "$LAUNCH_DIR/.env" ]; then
|
|
621
|
+
if [ -f "$LAUNCH_DIR/.env.example" ]; then
|
|
622
|
+
cp "$LAUNCH_DIR/.env.example" "$LAUNCH_DIR/.env"
|
|
623
|
+
_warn "launch-dashboard/.env created from .env.example — FILL IN LAUNCH_PASSWORD before it can start"
|
|
624
|
+
else
|
|
625
|
+
_bad "launch-dashboard/.env is missing and there is no .env.example to create one from"
|
|
626
|
+
FAILED=1
|
|
627
|
+
fi
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
_LD_PORT="$(grep -E '^LAUNCH_UI_PORT=' "$LAUNCH_DIR/.env" 2>/dev/null | tail -1 | cut -d= -f2)"
|
|
631
|
+
_LD_PORT="${_LD_PORT:-8099}"
|
|
632
|
+
_LD_PROJECT="$(isolated_project_name "$ROOT" launch)"
|
|
633
|
+
_LD_HEALTH_URL="http://localhost:${_LD_PORT}/api/health"
|
|
634
|
+
|
|
635
|
+
if [ ! -f "$LAUNCH_DIR/.env" ]; then
|
|
636
|
+
LAUNCH_STATUS=failed
|
|
637
|
+
elif [ "$CHECK_ONLY" = "1" ]; then
|
|
638
|
+
if wait_for_health "$_LD_HEALTH_URL" 3 1; then
|
|
639
|
+
LAUNCH_STATUS=up
|
|
640
|
+
_ok "up at $_LD_HEALTH_URL (project: $_LD_PROJECT)"
|
|
641
|
+
else
|
|
642
|
+
LAUNCH_STATUS=not-answering
|
|
643
|
+
_warn "not answering at $_LD_HEALTH_URL"
|
|
644
|
+
fi
|
|
645
|
+
elif ! runtime_up; then
|
|
646
|
+
LAUNCH_STATUS=no-runtime
|
|
647
|
+
_warn "no container runtime is running — launch dashboard unavailable, THE PIPELINE STILL RUNS"
|
|
648
|
+
else
|
|
649
|
+
# ISOLATED FROM EVERY OTHER INSTALL ON THIS MACHINE, DETERMINISTICALLY. Two checkouts (a
|
|
650
|
+
# dev tree and a dogfood copy, say) must both be able to run at once. Retries on a subnet
|
|
651
|
+
# OR a port collision — found live, both are real: a second install's observability stack
|
|
652
|
+
# hit the subnet default, and THIS stack hit LAUNCH_UI_PORT's default (8099) already held
|
|
653
|
+
# by an earlier install on the same machine. Attempt 0 keeps the .env-declared port exactly
|
|
654
|
+
# (no surprise for an operator who set one deliberately); only a genuine collision steps to
|
|
655
|
+
# the next offset. Any OTHER failure (a bad Dockerfile, a missing image) would fail
|
|
656
|
+
# identically on every candidate, burning through all of them and hiding the real error.
|
|
657
|
+
_LD_UP=1
|
|
658
|
+
_LD_LOG="$(mktemp)"
|
|
659
|
+
_LD_SUBNET=""; _LD_I=0
|
|
660
|
+
for _LD_SUBNET in $(isolated_subnet_candidates "$ROOT"); do
|
|
661
|
+
_LD_TRY_PORT=$((_LD_PORT + _LD_I * 10))
|
|
662
|
+
if (cd "$LAUNCH_DIR" && LAUNCH_SUBNET="$_LD_SUBNET" LAUNCH_UI_PORT="$_LD_TRY_PORT" \
|
|
663
|
+
container_compose -f "$LAUNCH_COMPOSE" -p "$_LD_PROJECT" up -d --build) >"$_LD_LOG" 2>&1; then
|
|
664
|
+
_LD_UP=0
|
|
665
|
+
_LD_PORT="$_LD_TRY_PORT"
|
|
666
|
+
_LD_HEALTH_URL="http://localhost:${_LD_PORT}/api/health"
|
|
667
|
+
break
|
|
668
|
+
fi
|
|
669
|
+
grep -qiE 'overlap|pool|port is already allocated|address already in use' "$_LD_LOG" || break
|
|
670
|
+
# TEAR DOWN BEFORE THE NEXT ATTEMPT — same fix as the observability stack's retry
|
|
671
|
+
# loop: a failed `up -d` can still CREATE containers attached to the failed attempt's
|
|
672
|
+
# network/port before the failure, and compose does not cleanly re-attach an
|
|
673
|
+
# already-created container to a DIFFERENT network or port on a later `up`. Each
|
|
674
|
+
# attempt needs a clean slate.
|
|
675
|
+
(cd "$LAUNCH_DIR" && container_compose -f "$LAUNCH_COMPOSE" -p "$_LD_PROJECT" down) >/dev/null 2>&1 || true
|
|
676
|
+
_LD_I=$((_LD_I + 1))
|
|
677
|
+
done
|
|
678
|
+
|
|
679
|
+
if [ "$_LD_UP" = "1" ]; then
|
|
680
|
+
LAUNCH_STATUS=failed
|
|
681
|
+
_bad "launch dashboard failed to start: $(tail -3 "$_LD_LOG" 2>/dev/null)"
|
|
682
|
+
FAILED=1
|
|
683
|
+
elif wait_for_health "$_LD_HEALTH_URL" "$LAUNCH_HEALTH_TRIES" "$LAUNCH_HEALTH_INTERVAL"; then
|
|
684
|
+
LAUNCH_STATUS=up
|
|
685
|
+
_ok "up and healthy at $_LD_HEALTH_URL (project: $_LD_PROJECT, subnet: $_LD_SUBNET)"
|
|
686
|
+
else
|
|
687
|
+
LAUNCH_STATUS=unhealthy
|
|
688
|
+
_bad "containers started but never answered healthy at $_LD_HEALTH_URL"
|
|
689
|
+
FAILED=1
|
|
690
|
+
fi
|
|
691
|
+
rm -f "$_LD_LOG" 2>/dev/null
|
|
692
|
+
fi
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
# ── The command people will actually type ───────────────────────────────────
|
|
696
|
+
# ── What this install IS ──────────────────────────────────────────────────────
|
|
697
|
+
# An install whose mode can only be inferred from which containers happen to be running is an
|
|
698
|
+
# install nobody can reason about later. install.sh --check reads this rather than re-deriving.
|
|
699
|
+
if [ "$CHECK_ONLY" = "0" ]; then
|
|
700
|
+
cat > "$ROOT/install-manifest.json" <<MANIFEST
|
|
701
|
+
{
|
|
702
|
+
"stack": "${STACK}",
|
|
703
|
+
"runner": "${RUNNER}",
|
|
704
|
+
"containerRuntime": "${CONTAINER_RUNTIME}",
|
|
705
|
+
"dashboards": "${USE_DOCKER}",
|
|
706
|
+
"replay": "${REPLAY_MODE}",
|
|
707
|
+
"project": "${EPAM_PROJECT:-}",
|
|
708
|
+
"installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
709
|
+
"installRoot": "${ROOT}",
|
|
710
|
+
"launchDashboard": "${LAUNCH_STATUS}"
|
|
711
|
+
}
|
|
712
|
+
MANIFEST
|
|
713
|
+
fi
|
|
714
|
+
|
|
715
|
+
_head "Result"
|
|
716
|
+
if [ "$FAILED" = "1" ]; then
|
|
717
|
+
_bad "install incomplete — fix the items marked ✗ above"
|
|
718
|
+
exit 1
|
|
719
|
+
fi
|
|
720
|
+
_ok "ready"
|
|
721
|
+
printf '\n Run a ticket: %s\n' "./orchestrations/scripts/pipeline --jira ABC-1234"
|
|
722
|
+
printf ' Check first: %s\n' "./orchestrations/scripts/pipeline --jira ABC-1234 --dry-run"
|
|
723
|
+
printf ' Switch stack: %s\n\n' "EPAM_PROVIDER_SET=<${ALL_SETS//,/|}> ./orchestrations/scripts/pipeline --jira ABC-1234"
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amsd-pipeline",
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "Installer for the amsd-pipeline orchestration stack. Clones, packages and provisions the full stack with one command — no separate git clone step.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"amsd-pipeline": "bin/amsd-pipeline.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"install.sh"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/dune94/epam-cli.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
}
|
|
20
|
+
}
|