alexandr 0.3.2 → 0.3.4
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/package.json +1 -1
- package/src/commands.js +18 -0
- package/src/docker.js +35 -2
- package/src/probe.js +11 -1
- package/src/updater.js +11 -0
- package/templates/docker-compose.yml +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexandr",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Run the alexandr workspace runtime locally (npx alexandr up), and develop alexandr apps against a workspace (alexandr app link | dev | build | deploy).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands.js
CHANGED
|
@@ -616,6 +616,9 @@ export async function update(flags) {
|
|
|
616
616
|
stampUpdaterEnv(inst);
|
|
617
617
|
}
|
|
618
618
|
const prevImage = readEnv(inst.dir).ALEXANDR_IMAGE || `${IMAGE_BASE}:latest`;
|
|
619
|
+
// What `.previous-image` said BEFORE this run — restored if the pull fails (see below).
|
|
620
|
+
const pointerPath = path.join(inst.dir, ".previous-image");
|
|
621
|
+
const priorPointer = fs.existsSync(pointerPath) ? fs.readFileSync(pointerPath, "utf8") : null;
|
|
619
622
|
const base = kernelUrl(kernelPort(inst.dir));
|
|
620
623
|
const before = (await kVersion(base))?.version ?? "?";
|
|
621
624
|
|
|
@@ -662,6 +665,21 @@ export async function update(flags) {
|
|
|
662
665
|
pull = compose(inst.dir, inst.projectName, ["pull", "kernel"]);
|
|
663
666
|
}
|
|
664
667
|
if (pull.status !== 0) {
|
|
668
|
+
// ⚠ PUT THE PIN BACK. The new tag was written into .env before the pull so compose would
|
|
669
|
+
// pull it; a failed pull must not leave the box pointing at an image it does not have —
|
|
670
|
+
// the next `up` would then boot the new version behind the operator's back, which is
|
|
671
|
+
// exactly what the first sidecar press on aos did (2026-09-08). `.previous-image` is
|
|
672
|
+
// restored too, so a later `--rollback` still means what it says.
|
|
673
|
+
setEnv(inst.dir, "ALEXANDR_IMAGE", prevImage);
|
|
674
|
+
if (priorPointer === null) {
|
|
675
|
+
try {
|
|
676
|
+
fs.unlinkSync(pointerPath);
|
|
677
|
+
} catch {
|
|
678
|
+
/* nothing was recorded before this run either */
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
fs.writeFileSync(pointerPath, priorPointer);
|
|
682
|
+
}
|
|
665
683
|
fail("Pull failed — check your network / the image tag.", EXIT.RUNTIME);
|
|
666
684
|
}
|
|
667
685
|
}
|
package/src/docker.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// `docker compose` against the published kernel image. Pure Node child_process.
|
|
3
3
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
7
8
|
import { EXIT } from "./exit.js";
|
|
@@ -38,12 +39,44 @@ function baseArgs(dir, projectName) {
|
|
|
38
39
|
];
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* ⚠⚠ THE INSTANCE'S `.env` IS THE ONLY SOURCE COMPOSE INTERPOLATES FROM. Compose gives the
|
|
44
|
+
* PROCESS environment precedence over the project's `.env`, and the updater sidecar's own
|
|
45
|
+
* environment is that very `.env`, frozen at the container's start (`env_file` on the service).
|
|
46
|
+
* So the recipe run from inside pinned `ALEXANDR_IMAGE=v0.0.13` in the file, then compose pulled
|
|
47
|
+
* and recreated v0.0.12 from the sidecar's stale variable — "Updated: 0.0.12 → 0.0.12", every
|
|
48
|
+
* press, including the one the 2026-09-08 E2E called a success (found on the first human press,
|
|
49
|
+
* aos, 2026-09-09). The same shadowing bites a host shell that exported `ALEXANDR_IMAGE` once.
|
|
50
|
+
* Every key the file defines is dropped from the child's environment; explicit `overrides`
|
|
51
|
+
* (a caller's deliberate one-off) still win.
|
|
52
|
+
*/
|
|
53
|
+
export function composeEnv(dir, base = process.env, overrides = {}) {
|
|
54
|
+
const env = { ...base };
|
|
55
|
+
for (const key of envFileKeys(dir)) delete env[key];
|
|
56
|
+
return { ...env, ...overrides };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The KEYS the instance's `.env` defines (uncommented lines only) — a hand copy of the
|
|
60
|
+
// instance module's parser, kept here because that module imports this one.
|
|
61
|
+
function envFileKeys(dir) {
|
|
62
|
+
try {
|
|
63
|
+
return fs
|
|
64
|
+
.readFileSync(path.join(dir, ".env"), "utf8")
|
|
65
|
+
.split("\n")
|
|
66
|
+
.map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=/))
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map((m) => m[1]);
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
// Run a compose subcommand, inheriting stdio by default (streams to the user).
|
|
42
75
|
export function compose(dir, projectName, args, opts = {}) {
|
|
43
76
|
const r = spawnSync("docker", [...baseArgs(dir, projectName), ...args], {
|
|
44
77
|
stdio: opts.stdio ?? "inherit",
|
|
45
78
|
encoding: "utf8",
|
|
46
|
-
env:
|
|
79
|
+
env: composeEnv(dir, process.env, opts.env || {}),
|
|
47
80
|
});
|
|
48
81
|
return { status: r.error ? 127 : r.status ?? 1 };
|
|
49
82
|
}
|
|
@@ -52,7 +85,7 @@ export function compose(dir, projectName, args, opts = {}) {
|
|
|
52
85
|
export function composeCapture(dir, projectName, args, env = {}) {
|
|
53
86
|
const r = spawnSync("docker", [...baseArgs(dir, projectName), ...args], {
|
|
54
87
|
encoding: "utf8",
|
|
55
|
-
env:
|
|
88
|
+
env: composeEnv(dir, process.env, env),
|
|
56
89
|
});
|
|
57
90
|
return {
|
|
58
91
|
status: r.error ? 127 : r.status ?? 1,
|
package/src/probe.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Health/version probing of a running kernel over its loopback port. Uses the
|
|
2
2
|
// global fetch (Node 18+). All probes are best-effort and never throw.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Where the kernel answers from HERE. On the host that is the loopback port compose mapped;
|
|
6
|
+
* inside the updater sidecar 127.0.0.1 is the sidecar itself, so the compose file sets
|
|
7
|
+
* `ALEXANDR_KERNEL_URL=http://kernel:3030` (the service name on the project network) — without
|
|
8
|
+
* it the recipe's health wait could not see the kernel from inside and reported `? → ?`
|
|
9
|
+
* (the first sidecar press on aos, 2026-09-08).
|
|
10
|
+
*/
|
|
11
|
+
export const kernelUrl = (port = 3030, env = process.env) => {
|
|
12
|
+
const explicit = (env.ALEXANDR_KERNEL_URL || "").trim();
|
|
13
|
+
return explicit ? explicit.replace(/\/+$/, "") : `http://127.0.0.1:${port}`;
|
|
14
|
+
};
|
|
5
15
|
|
|
6
16
|
async function getJson(url, timeoutMs = 2500) {
|
|
7
17
|
const ctrl = new AbortController();
|
package/src/updater.js
CHANGED
|
@@ -124,6 +124,17 @@ export function createUpdaterHandler(deps) {
|
|
|
124
124
|
} catch (e) {
|
|
125
125
|
result = { ok: false, error: e?.message || String(e) };
|
|
126
126
|
}
|
|
127
|
+
// ⚠ THE RECEIPT IS THE VERDICT, NOT THE EXIT CODE. `alexandr update` exits 0 after
|
|
128
|
+
// recreating whatever image compose resolved; when that is not the target, the box is
|
|
129
|
+
// "updated" onto the version it already ran (the 2026-09-09 press: `Updated: 0.0.12 →
|
|
130
|
+
// 0.0.12` with 0.0.13 asked for — the sidecar's own environment shadowed the pin). A done
|
|
131
|
+
// that names the wrong version is a failure with words, and the sheet offers the way back.
|
|
132
|
+
if (result.ok && !rollback && to && result.to && result.to !== to) {
|
|
133
|
+
result = {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `the box came back on ${result.to}, not ${to} — the recipe recreated the image it already ran`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
127
138
|
if (result.ok) {
|
|
128
139
|
write({ state: "done", phase: "done", to: result.to ?? to, from: result.from ?? null, rollback, startedAt, finishedAt: now() });
|
|
129
140
|
} else {
|
|
@@ -48,6 +48,9 @@ services:
|
|
|
48
48
|
ALEXANDR_INSTANCE_DIR: ${ALEXANDR_INSTANCE_DIR:-/nonexistent}
|
|
49
49
|
ALEXANDR_COMPOSE_PROJECT: ${ALEXANDR_COMPOSE_PROJECT:-alexandr}
|
|
50
50
|
ALEXANDR_UPDATER_SOCKET: /run/alexandr/updater.sock
|
|
51
|
+
# The kernel, by its service name: from inside this container 127.0.0.1 is the sidecar
|
|
52
|
+
# itself, and the recipe's health wait must see the kernel come back.
|
|
53
|
+
ALEXANDR_KERNEL_URL: http://kernel:3030
|
|
51
54
|
env_file:
|
|
52
55
|
- path: ./.env
|
|
53
56
|
required: false # ALEXANDR_CP_URL / ALEXANDR_RELEASES_URL → the feed it reads
|