Skip to content
Merged

Dev #320

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c368b34
BUGS: stacker install writes no .env, .env.example defaults missing
robotizeit Oct 19, 2026
d205f14
Bake channels: a bake reaches buyers only after its smoke clone passes
robotizeit Oct 26, 2026
44d862a
Bake only the caller's own build box; no redirects in the health probe
robotizeit Oct 26, 2026
6a07988
Bake channels: the smoke clone reaches the unpromoted bake
robotizeit Oct 28, 2026
8f7070d
tests: a test process drops the databases it created
robotizeit Oct 28, 2026
222667a
Merge pull request #313 from trydirect/fix/tests-drop-their-databases
vsilent Oct 29, 2026
11f3c7c
Merge pull request #305 from trydirect/docs/bugs-install-no-env-defaults
vsilent Oct 29, 2026
4d621f3
BUGS: bake finalize drops volumes before the conflict check (#13); pr…
robotizeit Oct 26, 2026
d52efd3
BUGS: clone cpx11 default (#15), lock ssh_key null (#16)
robotizeit Oct 26, 2026
35c9c3a
BUGS: bake done before the image is available (#17)
robotizeit Oct 26, 2026
22d996c
BUGS #18: no generated type satisfies a symbol-required password rule
robotizeit Oct 27, 2026
1739248
BUGS #19: cloud deploy drops the executable bit of project files
robotizeit Oct 27, 2026
db81b0c
BUGS #20: redeploy reports port_conflict on the stack's own port
robotizeit Oct 27, 2026
eb76198
BUGS #21: server-target bake records no server type
robotizeit Oct 27, 2026
a84f0f7
BUGS #22: CLI project lookup lists the whole account under a 30s timeout
robotizeit Oct 27, 2026
4e018a7
BUGS #20: also on a fresh cloud deploy
robotizeit Oct 28, 2026
a36dddd
BUGS #23: resubmit keeps the old contract; bakes use it
robotizeit Oct 28, 2026
9f1be2f
BUGS: withdraw #23, the resubmit was refused, not ignored
robotizeit Oct 28, 2026
56d09c8
Merge pull request #312 from trydirect/fix/bake-only-own-build-box
vsilent Oct 29, 2026
fa05c42
Merge pull request #311 from trydirect/docs/bugs-bake-finalize-order
vsilent Oct 29, 2026
51d1189
Merge pull request #310 from trydirect/feature/bake-channels
vsilent Oct 29, 2026
963855d
Keep credentials out of the auth spans
robotizeit Oct 15, 2026
73b5ff2
Merge pull request #293 from trydirect/feature/me-roles
vsilent Oct 29, 2026
e8c4c3f
Quick Deploy clone: a root admin may smoke-clone an unpromoted bake
robotizeit Oct 30, 2026
2866d64
OAuth: an unreachable auth server refuses the token, whatever its add…
robotizeit Oct 30, 2026
9f70ade
Configuration: a file that does not parse stops the service
robotizeit Oct 30, 2026
fb64a9c
Merge pull request #314 from trydirect/fix/clone-unstable-root-admin
vsilent Oct 30, 2026
ad9690f
Merge pull request #315 from trydirect/fix/oauth-no-test-user-fallback
vsilent Oct 30, 2026
a251541
Merge pull request #316 from trydirect/fix/config-parse-error-is-fatal
vsilent Oct 30, 2026
e96c65e
Vendor avatar is derived via Gravatar; website_url must be http(s)
robotizeit Oct 30, 2026
a61bfa1
Merge pull request #317 from trydirect/fix/vendor-avatar-gravatar
vsilent Oct 30, 2026
5b309c0
release: bump version to 0.3.5
robotizeit Nov 2, 2026
e4c4700
chore: sync demo assets into dist
robotizeit Nov 2, 2026
cbbc3b5
docs: record post-0.3.5 fixes in changelog
robotizeit Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions BUGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,217 @@ message also hides the cause: it does not say which version was asked for.
- Should a missing exact version fall back to the latest healthy snapshot
(with a warning), or stay a hard error with the requested version in the
message?

## 12. 🟠 OPEN — `stacker install <template>` writes no `.env`, so `${VAR}` defaults from `.env.example` are missing (found 2026-10-19)

**Severity:** Medium - a marketplace template whose `stacker.yml` reads a
plain (non-secret) value through `${VAR}` cannot be deployed after
`stacker install`.

### Reproduced (2026-10-19, CLI `1cd524e`)

```console
$ stacker install local-ai-packaged --domain local-ai-packaged.try.direct --key <key> --region fsn1 --file ./stacker.yml --force
OK: stacker.yml written
$ stacker deploy --target cloud --key <key> --force-new --no-hooks
✗ Environment variable not found: $FLOWISE_USERNAME
```

`FLOWISE_USERNAME=admin` lives only in the template's `.env.example`; the
install writes `stacker.yml` and nothing else, so every `${VAR}` whose value
is a default in `.env.example` (not a contract-generated secret) is unset.
Deploying from the template's own directory (with `.env` created from
`.env.example`) works.

### Expected

`stacker install` materializes the template's `.env.example` defaults (and
the contract's generated values) into `.env`, or the deploy treats a
`.env.example` default as the value.
## 13. 🟠 NOT CONFIRMED — bake finalize drops the build box's volumes before it checks for secret conflicts (found 2026-10-26)

### Observed (2026-10-26)

`POST /api/admin/bake` on a healthy build box failed with "the same secret value
is declared under 2 protected fields (EMBEDDING_BINDING_API_KEY,
LLM_BINDING_API_KEY)". The report said the stack was already stopped and its data
volumes dropped, so the box could not be baked again and a new one had to be
deployed.

### Root cause (by reading `src/helpers/bake_finalize.rs`, not yet confirmed by a test)

`finalize_build_box` runs the volume reset (stage 2, around line 484) before
`parameterize_embedded_secret_values` (stage 3), which is where the conflict is
detected. The conflict depends only on the compose and `.env` already read in
stage 1, so it could be checked before anything destructive runs.

### Fix direction

Run the conflict check (and the other checks that need only the files read in
stage 1) before the teardown, and keep the stage 3 rewrite where it is. Test:
a build box whose `.env` has two protected keys with one value fails without
running any volume reset command.

## 14. 🟡 NOT CONFIRMED — the secret-conflict check also rejects `provided` keys that share a value (found 2026-10-26)

### Observed (2026-10-26)

Same bake as #13. Both keys are `mutability: provided`: the buyer enters them on
Quick Deploy, and nothing regenerates them. A buyer can reasonably use one OpenAI
key for both the LLM and the embedding endpoint. The message says "each is
regenerated independently per buyer", which holds for `generated` keys only.

### Open questions before a fix

- For `provided` keys, the bake parameterizes embedded copies by value. When two
`provided` keys share a value, should the embedded copy go to either key, or
should bake still refuse but say why (the template author used one value for
two buyer inputs)?
- At minimum the message should not claim regeneration for `provided` keys.

## 15. 🟠 NOT CONFIRMED — clone defaults to `cpx11`, which Hetzner no longer offers in fsn1/nbg1/hel1 (found 2026-10-26)

### Observed (2026-10-26)

`POST /api/v1/deploy/clone` without `server_type` failed with Hetzner 422
"unsupported location for server type". The Hetzner API lists `cpx11` as
deprecated and unavailable in fsn1, nbg1, hel1 and sin (available only in ash and
hil). The same request with `server_type: cpx22` succeeded.

### Root cause (by reading)

`default_server_type()` in `src/routes/oneclick_deploy/clone.rs` returns
`"cpx11"`, and `default_region()` returns `"fsn1"`, so the two defaults together
can no longer create a server.

### Fix direction

Default to the server type the snapshot was baked on (or the stack's
`deploy.cloud.size`), not a fixed name. Test: a clone request without
`server_type` uses the snapshot's type.

## 16. 🟡 NOT CONFIRMED — the cloud deployment lock records `ssh_key: null` (found 2026-10-26)

### Observed (2026-10-26)

After `stacker deploy --target cloud --key <saved cloud>`, the
`.stacker/deployment-cloud.lock` had `ssh_key: null`, although the CLI had just
saved the server's key to `~/.config/stacker/ssh/server-<id>_ed25519` and that
key logs in. Tools that read the lock to reach the server (for example a bake
script) cannot connect.

### Fix direction

Write the saved key path into the lock. Test: after a cloud deploy, the lock's
`ssh_key` points to an existing key file.

## 17. 🟠 NOT CONFIRMED — a bake reports `done` and offers the snapshot while the Hetzner image is still `creating` (found 2026-10-26)

### Observed (2026-10-26)

`GET /api/admin/bake/{id}` returned `done` with an `image_id`. A Quick Deploy started
right after failed; the clone call answered 502 "Hetzner clone failed" with Hetzner
422 `image not yet available`, and the Hetzner API showed the image in status
`creating`. The buyer-facing message was only "Deploy failed: 400: Bad Request".

### Fix direction

Mark the bake `done` (and the snapshot resolvable for clone) only after the image is
`available`, or have clone answer a clear "snapshot still being prepared, retry in a
few minutes" instead of a generic failure. Test: a snapshot whose image is
`creating` is not offered to clone.

## 18. 🟡 NOT CONFIRMED — no generated secret type can satisfy a "must contain a symbol" password rule (found 2026-10-27)

### Observed (2026-10-27)

PipesHub refuses an admin password without an uppercase letter, a lowercase letter,
a digit and one of `#?!@$%^&*-`. `generator_shell_expression`
(`src/console/commands/cli/init.rs`) offers `hex`, `base64` (`+/=` only) and
`alphanumeric`, none of which guarantees those classes, so the password has to be
`provided` by the buyer instead of generated. A field `pattern` cannot express the
rule either: patterns compile with the `regex` crate, which has no look-ahead
("look-around ... is not supported").

### Fix direction

A `password` generated type that guarantees one character of each class (with a
configurable symbol set), so stacks with complexity rules can generate the admin
password per clone. Test: 1000 generated values all match the four classes.

## 19. 🟡 NOT CONFIRMED — a cloud deploy uploads project files without their executable bit (found 2026-10-27)

### Observed (2026-10-27)

`scripts/n8n-entrypoint.sh` is `-rwxr-xr-x` in git and locally. After
`stacker deploy --target cloud` it was `-rw-r--r--` in
`/home/trydirect/project/scripts/` on the server, so a container that bind-mounts
it and execs it directly crash-looped with
`exec /docker-entrypoint.sh failed: Permission denied`.

### Fix direction

Keep the source file mode when uploading project files (at least the owner
execute bit). Test: an executable file in the project arrives executable on the
server.

## 20. 🟡 NOT CONFIRMED — a redeploy to the same server ends `paused [port_conflict]` on the stack's own port (found 2026-10-27)

### Observed (2026-10-27)

`stacker deploy --target server` of a changed stack onto the server it already
ran on: deployments #1513 and #1514 both ended `paused [port_conflict]`. Right
after #1514 the stack's own new container held the port (`Up 36 seconds
(healthy)`, `0.0.0.0:5678->5678`), no other container used it, and the app
answered normally. The redeploy itself worked; only its status says it did not.

2026-10-28: the same on a fresh cloud deploy (#1517, new Hetzner server, no
earlier stack on it): `paused [port_conflict]`, while the stack was up and
healthy on its port and nothing else held it. The lock also recorded
`server_ip: null` although the CLI printed the IP.

### Fix direction

Exclude the containers of the stack being deployed (same compose project) when
checking whether its ports are taken. Test: redeploying a running stack with
unchanged ports does not report a port conflict.

## 21. 🟠 NOT CONFIRMED — a bake of a `--target server` deployment records no server type, so Quick Deploy offers the default size (found 2026-10-27)

### Observed (2026-10-27)

n8n 2.0.0 was baked from a build box created as a Hetzner `cpx22`, but redeployed
with `stacker deploy --target server` before the bake. Quick Deploy prepare then
offered `cpx32` ($26/mo) instead of `cpx22`. `admin_bake.rs` records the snapshot's
`server_type` from the deployment's server row (`p.server.server`); the consumer
falls back to its default (`cpx32`) when it is empty. Guess, not checked in the
DB: the server row of a server-target deployment has no type.

### Fix direction

Take the snapshot's server type from the provider (the Hetzner server the image was
made from), not from the deployment's server row, or refuse a bake whose server
type is unknown. Test: a bake of a box with no recorded type stores the provider's
type.

## 22. 🟡 NOT CONFIRMED — the CLI finds a project by downloading every project of the account under a 30 s timeout (found 2026-10-27)

### Observed (2026-10-27)

`stacker deploy --target cloud` failed twice at "Resolving project" with
"request or response body error: operation timed out". `find_project_by_name`
(`src/cli/stacker_client.rs`) calls `list_projects()` and filters locally. For an
account with 367 projects the response is 4.9 MB (single projects up to 0.7 MB);
it took 3 s once and was still streaming after 120 s another time, against the
client's 30 s timeout. No server was created by the failed runs.

### Fix direction

Look the project up by name on the server (a filtered endpoint, or the lock's
`project_id` when present) instead of listing the whole account, and keep list
responses free of full project bodies. Test: resolving a project does not fetch
other projects.

## 23. ❌ WITHDRAWN - "resubmit keeps the old version's contract"

Not a bug. The resubmit was refused (400, `MARKETPLACE_CONFIG_CONTRACT_SECRET_POLICY_REQUIRED`), so no new version was created and the bake used the only stored one. The refusal was hidden by an output filter on the reporter's side.
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Fixed — Marketplace vendor profile safety

- The public vendor profile derives `avatar_url` from the account email via Gravatar (identicon) and ignores the client-supplied value; a migration clears stored non-Gravatar URLs.
- `website_url` on a vendor profile now rejects non-http(s) schemes before it is rendered into a link.

### Fixed — Fail-closed configuration and OAuth

- A configuration file that does not parse stops the service instead of silently starting on defaults; with no configuration file at all, defaults still apply.
- OAuth refuses the token when the auth server is unreachable, at any address — the loopback/localhost test-user fallback is gone.

### Fixed — Quick Deploy clone

- A root admin may smoke-clone an unpromoted bake (`channel: "unstable"`), matching the access that already allowed `POST /api/admin/bake`.

## [0.3.5] — 2026-10-19

### Added — Managed server billing

- Quick Deploy Managed servers record usage per server with daily rates, day-by-day reports, a meter sweeper, and suspension of unpaid VMs; clone bills the Managed server before it runs.
- The User Service connector reports Managed server usage and billing events; internal endpoints expose the usage data.

### Added — Teams and roles

- Share projects with members and invite them by email; the owner decides whether members get server access.
- Members can list the project's servers and see their own deployments; the User Service learns which role a user holds on a deployment; a root-role user is treated as admin regardless of `/me` order.

### Added — Configuration and services

- `config_contract` fields accept `pattern` and `max_length`; `length` now means exactly that length.
- `stacker.yml` services support `restart`, `entrypoint`, `profiles`, `aliases`, `deploy`, `cap_add`, `cap_drop`, and `devices`; `deploy.replicas` accepts a `${VAR}` reference or a device count.

### Added — Marketplace and bake

- Start a bake from the API (`POST /api/admin/bake`); the bake is built in CI and shipped in the Docker image, picks its build box by Hetzner `server_id`, and records the server type that `validate` returns with the rates.
- Quick Deploy is plan-independent: no Pro check on the one-click path.

### Fixed — Security and authorization

- Admin service JWT signatures are verified; agent routes respond 401 instead of 500; deployment, project, cloud, and command ownership is checked across the API and marketplace purchase tokens must be plain.
- A marketplace resubmit no longer silently makes a stack free, and the free-deploy access gate is closed.

### Fixed — Deploy correctness

- Failed deploys exit 1 and print the failure verdict once; `config validate` fails on error-severity issues; unknown fields in `app:` are denied.
- Generated compose bind mounts resolve against the project root; host IPs of loopback port mappings stay in the deploy payload and off the internet; Hetzner availability reads from `server_types.locations`; preflight expands port ranges and resolves `${VAR:-default}` before probing.
- Destroy reports a remote stack instead of "absent" and never tears down under a guessed project name; a fresh box does not boot without its schema.

### Improved — Operations and CI

- Vault health checks use the same TLS settings as the Vault client; admin notifications cover verification-status changes and unapprove reasons; CI frees `target/debug` before the release build; `casbin_admin_template_update` has its own migration version.

## [0.3.4] — 2026-09-24

### Added — Marketplace config contracts
Expand Down
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "stacker"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
default-run= "server"

Expand Down Expand Up @@ -72,6 +72,8 @@ tokio-stream = "0.1.14"
actix-http = "3.4.0"
hmac = "0.12.1"
sha2 = "0.10.8"
# Gravatar avatar URLs for marketplace vendor profiles (helpers::gravatar).
md-5 = "0.10"
# Constant-time comparison for credential digests (helpers::agent_token).
subtle = "2.6"
sqlx-adapter = { version = "1.8.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]}
Expand Down
3 changes: 3 additions & 0 deletions migrations/20261026100000_baked_snapshot_channels.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v1 = '/api/admin/bake/:id/promote';
ALTER TABLE baked_snapshots DROP COLUMN IF EXISTS gated;
DROP TABLE IF EXISTS baked_snapshot_channels;
23 changes: 23 additions & 0 deletions migrations/20261026100000_baked_snapshot_channels.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- Release channels for baked snapshots (BAKE_PIPELINE_PLAN.md, Stage 4): the
-- pointers helpers/bake_registry.rs moves. A bake started from the API lands on
-- `unstable`; buyers get `latest` (promoted after a green smoke clone) or, when
-- that is unhealthy, `stable` (the previous `latest`).
CREATE TABLE IF NOT EXISTS baked_snapshot_channels (
stack TEXT NOT NULL,
provider TEXT NOT NULL,
channel TEXT NOT NULL,
snapshot_id INTEGER NOT NULL REFERENCES baked_snapshots (id),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (stack, provider, channel)
);

CREATE INDEX IF NOT EXISTS idx_baked_snapshot_channels_snapshot
ON baked_snapshot_channels (snapshot_id);

-- A gated snapshot reaches buyers only through `latest` or `stable`. Rows
-- recorded before this migration, and by the `bake` binary, stay ungated and
-- keep the "newest healthy" behaviour.
ALTER TABLE baked_snapshots ADD COLUMN IF NOT EXISTS gated BOOLEAN NOT NULL DEFAULT FALSE;

INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES ('p', 'group_admin', '/api/admin/bake/:id/promote', 'POST', '', '', '') ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES ('p', 'admin_service', '/api/admin/bake/:id/promote', 'POST', '', '', '') ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Data cleanup migration: cleared external avatar URLs cannot be restored.
9 changes: 9 additions & 0 deletions migrations/20261029120000_null_non_gravatar_avatars.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Vendor avatars are now derived from the account email via Gravatar
-- (see helpers::gravatar and PATCH /mine/vendor-profile). Stored values
-- from before that change may point at arbitrary external hosts, which
-- must not reach public pages: clear every non-Gravatar avatar_url. The
-- next profile save repopulates the column with the derived Gravatar URL.
UPDATE marketplace_vendor_profile
SET avatar_url = NULL
WHERE avatar_url IS NOT NULL
AND avatar_url !~* '^https://([a-z0-9-]+\.)?gravatar\.com/';
Loading
Loading