feat(presets): replace the nixpacks build engine with autopack - #530
Conversation
Nixpacks is gone as a dependency. The slugs it owned stay, and now build
through autopack.
Why now: nixpacks 1.41.0 is the latest published version and pulls two
advisories with no upstream fix available — RUSTSEC-2023-0018 (remove_dir_all
0.5.3, via tempdir 0.3.7) and GHSA-8wf9-4rjw-8j9r (serde_with 2.3.3). Both
were carried as documented, reachability-analysed exceptions because there was
nothing to upgrade to. Removing the dependency is the only thing that clears
them, and `cargo audit` now reports neither.
Compatibility is the constraint, not a nice-to-have. Existing projects have
`preset = 'nixpacks'` and a NixpacksConfig persisted against them, so:
* every `nixpacks*` slug still resolves, with the same stored_preset and the
same config shape;
* a persisted `nixpacks_config` TOML is still honoured — autopack reads the
Nixpacks schema in compatibility mode, and reports what it could not
translate instead of dropping it;
* a stored provider still forces that language, so a polyglot repository
pinned to `python` does not start building as `node` because detection
order differs.
Labels move from "Nixpacks (Python)" to "Autopack (Python)"; slugs do not.
Two behaviour changes worth knowing about:
* a project autopack cannot plan now produces a Dockerfile that exits 1 with
the reason. The previous fallback emitted `FROM alpine` + `COPY . .` and no
CMD, which builds and deploys cleanly into a container that exits at once;
* autopack's Dockerfiles need BuildKit for cache and secret mounts. The
deployment pipeline already enables it; a build that does not is refused by
name rather than failing later on a line the user never wrote.
Also adds tests/starters.rs, which builds every temps-examples starter through
the real preset, runs it, and requests a page — plus the two properties that
are invisible until they bite: that it answers on $PORT, and that it stops on
SIGTERM instead of waiting out the kill timeout.
📓 Changelog previewThis is what your commits will add to the generated ## [Unreleased]
### Added
- **presets:** Replace the nixpacks build engine with autopack
- **web:** Add the autopack preset icon
### CI
- Drop the temporary starters branch pin
### Fixed
- **ci:** Point the starters job at the branch that carries them
- **presets:** Stop the Nixpacks TOML validation error echoing the config
### Refactor
- **web:** Open the autopack icon's lid into a chevron
### Testing
- **ci:** Gate on rust/actix, which passes |
Every job in the matrix failed in 0.03s: `examples/starters` does not exist on temps-examples' default branch yet — it arrives with gotempsh/temps-examples#12 — so the checkout produced a tree without it and the test panicked before building anything. Two changes: * the checkout pins `ref: feat/language-starters`, marked TEMPORARY. It must come out when #12 merges; a branch ref that outlives its PR is a job that quietly stops testing what main ships. * the panic now distinguishes "the variable is unset" from "the directory is not there". They read identically and want completely different fixes, and the second one is what just cost a full CI run to diagnose.
It was parked in the report-only job on the strength of a local failure: on macOS it ignored SIGTERM and `docker stop` had to SIGKILL it. On Linux CI it passes cleanly in 104s, SIGTERM check included — so the failure was an artefact of Docker Desktop, not the starter. Leaving a passing starter in the report-only bucket means it is not actually gated, and a real regression in it would go unnoticed. Linux CI is the environment the gate is for. java/spring-boot stays behind: it still fails, for the same reason, every run.
CI green — and two corrections to the description aboveAll 24 starter jobs pass. Run 30823442411 Two things in the original description were wrong: 1. I understated the CI dependency. I wrote that four jobs would stay red pending gotempsh/temps-examples#12. It was all of them — 2. The four Still failing, by design not blocking
Merge orderThis PR cannot go in before gotempsh/temps-examples#12, and the |
gotempsh/temps-examples#12 is merged and the branch is deleted, so the `ref:` now points at nothing. The job reads `examples/starters` from the default branch, which is what it should have been testing all along.
`test_nixpacks_invalid_inline_toml_is_rejected_during_create` caught this: the new validation interpolated `toml`'s error Display, which renders the offending *source line*. The test feeds `secret_token = ["do-not-echo"` and asserts the value does not come back — it did. That message reaches an API response body and the logs, and a nixpacks_config can hold secrets, so this was a real leak rather than untidy output. Only the position crosses the boundary now: "failed to parse Nixpacks TOML at line 1, column 30; verify its syntax and supported fields". The position is derived from the error's span rather than its rendering, because both of the library's own accessors can carry content — Display embeds the source line, and `.message()` names the offending key on an unknown-field error. The full error goes to the debug log server-side, where it is useful and not exposed. Adds the same assertion as a unit test. The existing guard is behind `docker_available()`, so it only runs in the Docker integration job — a leak this cheap to reintroduce should fail in `cargo test -p temps-presets`.
Cargo.lock was the only conflict. Resolved by taking main's copy and re-resolving, rather than merging the two by hand: it is generated, and a hand-merged lockfile can be internally inconsistent in ways nothing checks. Verified afterwards that main's dependency bumps survived (aes-gcm 0.11.0, base64 0.23.0, md5 0.8.1, cron 0.17.0, validator 0.21.0, serial_test 4.0.1) and that nixpacks is still absent while the autopack pin is intact.
`AutopackPreset::icon_url` and the auto-detect Nixpacks variant both point at /presets/autopack.svg, which did not exist. The console falls back to custom.svg on error, so this was not a broken image — the new preset simply showed the generic mark, which is worse in a picker whose whole job is telling builders apart at a glance. The mark is an isometric box with its lid lifted. Not a triangle (Vercel's, and temps competes with them) and not a letter "A" (Astro's, and it sits three tiles away in this very picker). Teal because the picker already has purple, blue, green and orange.
Matches gotempsh/autopack. The closed cube was the generic registry package mark; the open lid reads as a box mid-pack and doubles as an up arrow. Teal rather than green is deliberate and checked against the real picker: Node.js owns green there, and autopack builds 24 ecosystems, so reading as a Node tool is misleading. Sky blue collides with Docker and Nixpacks, amber with HTML5. Teal is the only gap.
Replaces Nixpacks with autopack as the build engine. The
nixpacks*preset slugs stay exactly where they are — only what runs underneath them changed.Why now
nixpacks 1.41.0is the latest published version and pulls two advisories that have no upstream fix:remove_dir_all 0.5.3serde_with 2.3.3Both were carried as documented, reachability-analysed exceptions in the root
Cargo.tomlbecause there was nothing to upgrade to. Dropping the dependency is the only thing that clears them.Verified —
cargo auditbefore and after:The two exception blocks are deleted rather than reworded.
Compatibility is the constraint
Existing projects have
preset = 'nixpacks'and aNixpacksConfigpersisted against them. A deployment of one of those has to keep working with nobody touching anything:nixpacks*slug still resolves, with the samestored_presetand the same stored config shape;nixpacks_configTOML is still honoured — autopack reads the Nixpacks schema in compatibility mode, and surfaces anything it could not translate as a warning instead of dropping it silently;pythondoes not quietly start building asnodebecause detection order differs.Labels move from
Nixpacks (Python)toAutopack (Python). Slugs, which are persisted, do not move.Two behaviour changes worth review
A project that cannot be planned now fails loudly. The old fallback emitted
FROM alpine+COPY . .with noCMD— that builds, pushes, deploys, and then the container exits immediately with nothing useful in the log. It now emits a Dockerfile thatexit 1s with the actual reason.Autopack's Dockerfiles require BuildKit (cache and secret mounts).
build_image.rsalready setsuse_buildkit: true, so the deployment path is fine.temps-cli builddefaults it to false, and a classic-builder parse error points at a generated line the user never wrote — so that case is now refused by name up front. Worth a look: whethertemps buildshould just default to BuildKit instead.Tests: build it, run it, ask it for a page
crates/temps-presets/tests/starters.rstakes every starter fromgotempsh/temps-examples, renders it through the realAutopackPresetthe pipeline uses,docker builds it, runs it, and requests a page. The existing unit tests assert on generated Dockerfile text, which cannot tell a working build from a plausible-looking one.It also checks the two properties that are invisible until they bite:
$PORT— what the proxy connects to, and nothing else checks it;SIGTERMrather than waiting out the kill timeout. A container that ignores SIGTERM turns every redeploy into a hard kill of in-flight requests.Result: 23 of 25 starters pass end to end locally (
dockerfileis excluded — it exists to exercise the Dockerfile preset)..github/workflows/starters.ymlruns them as a matrix, one job per starter, plus nightly (the starters live in another repo, so a green PR here can still break later).The two that don't pass
They run in a separate always-green report job rather than as required checks — a red required check everyone learns to ignore is worse than no check.
java/spring-boot— Spring deduces aREACTIVEapplication type from a classpath that only hasspring-boot-starter-web, then fails for want of aReactiveWebServerFactory. The boot jar is selected correctly (the-plain.jarexclusion works) and the app does start, so this is not jar selection. Not yet root-caused.rust/actix— ignores SIGTERM;docker stopwaits the full grace period and SIGKILLs. Actix's default graceful-shutdown timeout outlives the stop timeout.One finding that belongs in temps-examples
ruby/rails,python/django,php/laravelanddotnet/webinitially failed. All four ship anixpacks.tomlpinning a nixpacks-specific start command, and autopack's compatibility mode honours them faithfully:rails server ...bundle exec, sorailsis not on PATH —exec: rails: not foundmanage.py migrate && gunicorn ...settings.DATABASES is improperly configuredphp artisan servephp8.3-pgsqlwhich does not resolvedotnet runAll four pass once the
nixpacks.tomlis removed — autopack's own providers already emitbundle exec rails server, gunicorn, FrankenPHP anddotnet <assembly>.dllcorrectly. Autopack already warns about three of these four via its risky-start-command check.Those files are nixpacks-specific tuning and should go once nixpacks does. That's a change to gotempsh/temps-examples#12, not to this PR — this PR's matrix will stay red on those four until it lands.
Verification
cargo check --workspace --lib— cleancargo test -p temps-presets --lib— 197 passed, 0 failedcargo clippy --all-targets -- -D warningson the touched crates — cleancargo audit— 2 advisories cleared, confirmed abovecargo tree -p temps-presets | grep nixpacks— no match; gone fromCargo.lockentirelyNot done
autopack-*are pinned to a commit on the public repo, not a released tag — a build plan changing under a release would change what every project's image contains with nothing here moving. Worth deciding whether to cut a tag before merge.