|
326 | 326 | would wait. The `env.sh` hook is also run directly, over a release-shaped directory, |
327 | 327 | so that what it selects and what environment it leaves behind are asserted by |
328 | 328 | observation rather than by reading the script. |
| 329 | +- `mix castle.relup`'s `--fromto`, `--upfrom` and `--downto` now take a |
| 330 | + *baseline spec*: one grammar naming the three places the release being upgraded |
| 331 | + from can come from. `rel:` is an assembled release, `tar:` is a shipped |
| 332 | + artefact, and `ref:` is a git ref that is checked out and built. A value with no |
| 333 | + prefix is a `rel:` path, so every invocation written before this means exactly |
| 334 | + what it meant then. The direction stays on the switch name and the source stays |
| 335 | + in the value. `--target` is not a spec: it names the release being generated |
| 336 | + for, which has just been assembled, and it says so if it is handed one. |
| 337 | + |
| 338 | + **`tar:` is the source to prefer, and the reason is correctness rather than |
| 339 | + convenience.** `release_handler` selects a relup entry by from-version *string* |
| 340 | + and never checks that the code actually running is the code the relup was |
| 341 | + generated against. A baseline rebuilt from source is built with today's Elixir, |
| 342 | + today's OTP and today's hex tarballs for whatever the lock does not fully pin, |
| 343 | + so its module set can differ from the one that is deployed — and where it does, |
| 344 | + the relup's instructions miss modules and the upgrade loads part of the new |
| 345 | + code over a system still running the rest of the old. A relup generated against |
| 346 | + a rebuilt baseline describes a transition from a release that never existed. |
| 347 | + |
| 348 | + `ref:` is still the right answer for development, for testing an upgrade path |
| 349 | + before anything ships, and for the common case where nobody kept the artefact — |
| 350 | + and it says on every use that what it produced was rebuilt rather than |
| 351 | + deployed. The commit is checked out into a git worktree, built there, and the |
| 352 | + worktree removed; what it built is kept, because none of it was ever inside the |
| 353 | + worktree. A shallow clone that does not hold the ref is named as such, with the |
| 354 | + `git fetch --tags --unshallow` that fixes it, rather than surfacing as an |
| 355 | + unknown revision from `git worktree add`. A project that does not sit at the top |
| 356 | + of its repository is built where it actually is. And because building an old |
| 357 | + commit runs that commit's own `mix.exs` — which in a project using Castle |
| 358 | + configures its release, and may want a relup of its own — `CASTLE_BASELINE` |
| 359 | + carries the sha being built, and a resolution that finds it set refuses rather |
| 360 | + than recursing. |
| 361 | + |
| 362 | + Both `tar:` and `ref:` keep what they produced under |
| 363 | + `_build/castle/baselines`, and every entry there is immutable: the work happens |
| 364 | + in a staging directory and the finished thing is renamed into place, so an |
| 365 | + entry exists only once it is whole, an interrupted run leaves nothing a later |
| 366 | + one would take for a usable baseline, and two runs resolving the same baseline |
| 367 | + at once each build their own with the first to finish winning. |
| 368 | + |
| 369 | + What an entry is keyed on is everything that could change its contents. A |
| 370 | + `tar:` artefact is keyed on a digest of its bytes rather than on its path, so a |
| 371 | + pipeline writing the same filename on every build is never served the previous |
| 372 | + build's release — and the artefact is copied before it is unpacked, so the bytes |
| 373 | + that were hashed are the bytes that get unpacked. A `ref:` baseline is keyed on |
| 374 | + the resolved commit *and* on what it was built with: which project inside the |
| 375 | + commit, the Mix environment and target, and the Elixir version, ERTS version |
| 376 | + and operating system. Without the toolchain versions, upgrading Elixir would |
| 377 | + leave every cached baseline compiled by the old one and the relup would be |
| 378 | + generated between module sets from two different compilers, which is the very |
| 379 | + drift that makes `tar:` the source to prefer; without the project, an |
| 380 | + umbrella's children would share one entry, since they share one `_build`. |
| 381 | + |
| 382 | + What that key cannot cover is stated rather than approximated: a `mix.exs` is |
| 383 | + arbitrary code and may read anything to decide what it builds. Where a build |
| 384 | + depends on something outside the key, name the artefact with `tar:` or clear |
| 385 | + `_build/castle/baselines`. |
| 386 | + |
| 387 | + A `tar:` artefact holding anything unpacking would not reproduce as itself is |
| 388 | + refused rather than unpacked in part. Erlang's tar reader writes regular files, |
| 389 | + directories and symlinks; a hard link is dropped and a device node or FIFO |
| 390 | + becomes an empty file, in both cases while still reporting success. A hard link |
| 391 | + is the one that turns up in practice — GNU tar writes one for the second copy |
| 392 | + of a file and a release tree has plenty of those — and it would come out as a |
| 393 | + missing `.beam` and a relup generated against a release short of modules. Two |
| 394 | + members that unpack to the *same* path are refused for the same reason: which |
| 395 | + of them survives is decided by the order they appear in rather than by the |
| 396 | + archive. |
| 397 | + |
| 398 | + Naming two *different* baselines for the same release version is refused too. |
| 399 | + A relup carries one entry per from-version and `release_handler` selects by |
| 400 | + version, so only one of them could ever be used and which one would depend on |
| 401 | + the order the switches were written in. Specs make that easy to reach without |
| 402 | + meaning to — `tar:my_app-1.0.0.tar.gz` beside `ref:v1.0.0` is a natural thing |
| 403 | + to write while checking the two agree, and they may not. One release named two |
| 404 | + ways is not that, and is not refused: the relative, absolute and symlinked |
| 405 | + spellings of one release are recognised as the one file. |
| 406 | + |
| 407 | + Two levels are available to callers: an appup coverage check needs only |
| 408 | + `mix compile` in the worktree, while a relup needs `mix release`, and on a real |
| 409 | + project that is a large difference. |
| 410 | + |
| 411 | + Nothing in this touches worktree registrations it did not make. `git worktree |
| 412 | + prune` would clear a stale one in a word, but it clears every stale one in the |
| 413 | + repository, and a checkout on a disk that is not mounted today looks exactly |
| 414 | + like a dead one — so only registrations inside the baseline cache are removed, |
| 415 | + and never a locked one. |
329 | 416 |
|
330 | 417 | ### Changed |
331 | 418 |
|
|
0 commit comments