|
| 1 | +--- |
| 2 | +"@objectstack/runtime": minor |
| 3 | +"@objectstack/cli": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(cli,runtime): `OS_ARTIFACT_URL` — boot a stack from a published artifact by reference (#8368) |
| 7 | + |
| 8 | +`objectstack start` / `serve` can now be pointed at an artifact **by reference** |
| 9 | +with a single environment variable, so a fixed runtime image plus one env var is |
| 10 | +a running app. Upgrading the app becomes an env change and a restart rather than |
| 11 | +an image rebuild — the runtime image and the app artifact become two independent |
| 12 | +release axes. |
| 13 | + |
| 14 | +```bash |
| 15 | +OS_ARTIFACT_URL=https://cdn.example.com/hotcrm-2.2.2.json # fetched at boot |
| 16 | +OS_ARTIFACT_URL=file:///srv/app/objectstack.json # read directly |
| 17 | +OS_ARTIFACT_URL='https://cdn.example.com/hotcrm-2.2.2.json#sha256=<64 hex>' # content-verified |
| 18 | +``` |
| 19 | + |
| 20 | +**One variable, not two.** The optional integrity pin is SRI-style and lives |
| 21 | +inside the URL **fragment**; there is deliberately no companion |
| 22 | +`OS_ARTIFACT_SHA256`. A fragment is client-side by standard and is never sent to |
| 23 | +the server, so the pin travels with the reference — one value to copy, one value |
| 24 | +to rotate — without changing anything the artifact host sees. A second variable |
| 25 | +would make "URL updated, hash not" a reachable state; this shape makes it |
| 26 | +unspellable. |
| 27 | + |
| 28 | +**Precedence.** `--artifact` > `OS_ARTIFACT_URL` > `OS_ARTIFACT_PATH` > |
| 29 | +`<cwd>/dist/objectstack.json`. Beating `OS_ARTIFACT_PATH` matters in practice: |
| 30 | +the official runtime image sets it to `/srv/app/objectstack.json`, so on a |
| 31 | +container carrying no app it is always set and always points at a file that does |
| 32 | +not exist. `OS_ARTIFACT_URL` also wins over an `objectstack.config.ts` in the |
| 33 | +working directory — naming a published artifact is an explicit instruction, and |
| 34 | +a deployed app must not depend on which directory the process is standing in. |
| 35 | + |
| 36 | +**What it refuses, and how loudly:** |
| 37 | + |
| 38 | +- **No pin → no verification.** A fetch or read failure fails the boot loudly so |
| 39 | + container orchestration retries. There is no cache-fallback on this path: with |
| 40 | + no pin there is nothing to authenticate a cached copy with. |
| 41 | +- **Pin present → verified before boot.** A mismatch refuses and names the |
| 42 | + **expected and the actual** digest, so a republished artifact is |
| 43 | + distinguishable from a substituted one. A fetch failure may fall back to a |
| 44 | + locally cached copy, but only one whose bytes still hash to the pin — the |
| 45 | + cache is re-hashed on every read, so the filename is never the authority — and |
| 46 | + it says so with a loud warning. |
| 47 | +- **`engines.protocol` is validated against the runtime** at reference |
| 48 | + resolution, before anything connects, and an incompatible artifact refuses |
| 49 | + with both ways out named (repoint the reference, or run a matching image). |
| 50 | +- **Migration policy.** Safe migrations run at boot; a destructive change (the |
| 51 | + `os migrate apply --allow-destructive` class) refuses the boot with an |
| 52 | + operator message naming every change. Never skipped in silence. This applies |
| 53 | + to the artifact-pinned boot only — every other boot keeps the standing |
| 54 | + production policy, under which the schema is never auto-altered. |
| 55 | + |
| 56 | +**Secrets.** The reference may be a pre-signed URL, i.e. the credential *is* the |
| 57 | +URL. Nothing downstream of resolution ever sees it: remote bytes are |
| 58 | +materialised to a local file under `<home>/artifacts` and the boot continues |
| 59 | +against that path, so the URL reaches neither the banner, nor the metadata |
| 60 | +service's artifact-source record, nor any log line. Every message this path |
| 61 | +produces — including messages originating inside `fetch`, which routinely carry |
| 62 | +the whole URL — is scrubbed of userinfo and query material. Userinfo is moved |
| 63 | +into an `Authorization: Basic` header, both because `fetch` refuses to construct |
| 64 | +a request from a URL carrying credentials and because a credential in the |
| 65 | +request line lands in the artifact host's access log. |
| 66 | + |
| 67 | +Materialising the fetched bytes is also what makes the pin mean anything: the |
| 68 | +bytes that were hashed are the bytes that boot, and the artifact is fetched |
| 69 | +exactly once. |
| 70 | + |
| 71 | +Not included, by design: `OS_PACKAGE_REF` registry resolution, signature |
| 72 | +enforcement and entitlements; multi-tenant fleet / hostname routing. Fetching |
| 73 | +and booting an artifact is open-framework mechanism — walled tenancy postures |
| 74 | +remain entitled through `@objectstack/organizations` regardless of how the |
| 75 | +artifact arrives. |
0 commit comments