Conversation
… fetches via HTTP, no curl needed in container)
… config.origin == MarketplaceGenerated (the file carries the # @stacker-origin: marketplace marker) unless the app section declares an explicit source (image/dockerfile/build). User-authored configs are completely unaffected — their app is always kept.
…ice_stack_from_members
install_catalog_application enriches the search summary from /applications/catalog via merge_catalog_enrichment, but that merge only overlaid single-app fields (docker_image, default_port(s), default_env, default_config_files) and dropped the stack's services[] and kind. The service-less summary then reached synthesize_catalog_compose, which took the single-app path and failed loud with "missing a dockerhub_image" for real multi-service stacks (e.g. LAMP) -- even though fetch_app_catalog fetched the 13-member payload successfully (no fallback). Overlay services[] and kind too. Add a regression test that runs the real flow (production LAMP fixture -> merge -> synthesize) and asserts a multi-service compose, plus a full-payload deserialize/synthesize test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR expands Stacker’s marketplace + CLI deployment workflows by adding per-install billing primitives (authorization ledger, capture/void hooks, and a background sweeper), improving catalog/marketplace install robustness (compose synthesis + embedded compose extraction), and enhancing CLI ergonomics (cloud override flags, service catalog inputs/healthchecks, and agent-network injection). It also adds Google Analytics tagging to the website HTML.
Changes:
- Add per-install billing flow: new DB table + user_service connector methods, install-time authorization persistence, deploy-complete capture hook, and an expiry sweeper.
- Improve install/deploy reliability for marketplace/catalog templates by synthesizing/locating compose content and avoiding phantom
appservices for services-only configs. - Enhance CLI: marketplace install cloud overrides + idempotency key threading, service catalog secrets-to-
.envflow, healthchecks, and shared-network injection for agent deployments.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/index.html | Adds Google tag (gtag.js) snippet to site source HTML. |
| website/dist/index.html | Adds the same Google tag snippet to built/dist HTML. |
| tests/marketplace_integration.rs | Updates webhook payload tests to use Default expansion. |
| tests/features/marketplace_per_install_billing.feature.pending | Adds pending BDD spec for per-install billing scenarios. |
| src/startup.rs | Spawns per-install billing sweeper during server startup (flag-gated). |
| src/services/mod.rs | Exposes install_authorization_sweeper module. |
| src/services/marketplace_access.rs | Adds per-install access gate via can_charge and a new NoPaymentMethod error variant + tests. |
| src/services/install_authorization_sweeper.rs | Implements background void/reconcile loop for expired authorizations. |
| src/routes/project/deploy.rs | Maps NoPaymentMethod to HTTP 402 and adds embedded compose extraction helper + tests. |
| src/routes/marketplace/search.rs | Enriches marketplace search results with creator_name. |
| src/routes/marketplace/public.rs | Captures per-install authorizations on deploy-complete; injects user_service into handler. |
| src/routes/marketplace/lamp_catalog_fixture.json | Adds production fixture for LAMP catalog payload round-trip tests. |
| src/routes/marketplace/install.rs | Adds idempotency + authorization fields to install API, per-install authorize/capture/void flow, and catalog compose synthesis + enrichment helpers + tests. |
| src/routes/marketplace/admin.rs | Fetches latest template version for approval webhook federation (best effort). |
| src/helpers/stacker_labels.rs | Adds sanitize_service_code helper for stable agent-facing service codes. |
| src/helpers/json.rs | Adds payment_required() helper for HTTP 402 responses. |
| src/db/mod.rs | Registers new marketplace_billing DB module. |
| src/db/marketplace.rs | Adds get_latest_version() helper for template approval/publish webhook federation. |
| src/db/marketplace_billing.rs | Implements per-install authorization persistence and sweeper lookup helpers. |
| src/console/commands/cli/service.rs | Adds service catalog input prompting + .env update helpers and tests. |
| src/console/commands/cli/secrets.rs | Updates test fixture for new StackerConfig.app_present field. |
| src/console/commands/cli/pipe.rs | Adds “direct container” fallback probing logic + tests, uses sanitize_service_code. |
| src/console/commands/cli/marketplace.rs | Adds vendor column, per-install idempotency key threading, cloud override flags, and plan display updates + tests. |
| src/console/commands/cli/init.rs | Updates default Hetzner size and marks init-generated configs as app_present=true. |
| src/console/commands/cli/deploy.rs | Enables agent shared-network injection when building deploy bundles for cloud/server deploys. |
| src/console/commands/cli/config.rs | Extends cloud setup wizard to optionally show/select saved cloud credential names. |
| src/console/commands/cli/agent.rs | Threads new build_config_bundle parameter for agent deploy flow. |
| src/connectors/user_service/types.rs | Adds per-install billing types (AuthorizationHandle, BillingCapability). |
| src/connectors/user_service/stack.rs | Avoids misusing stack_view OS images as container images; resolves stack docker images safely. |
| src/connectors/user_service/mod.rs | Re-exports new billing types. |
| src/connectors/user_service/mock.rs | Adds mock implementations for new connector methods. |
| src/connectors/user_service/marketplace_webhook.rs | Extends webhook payload with federated stack definition/config files/version + tests. |
| src/connectors/user_service/connector.rs | Extends trait with catalog enrichment + per-install billing methods. |
| src/connectors/user_service/client.rs | Implements catalog enrichment + per-install billing HTTP calls and shared error mapping. |
| src/connectors/user_service/app.rs | Adds typed services[] for stack catalog entries. |
| src/connectors/errors.rs | Adds PaymentRequired and Conflict variants and maps them to HTTP status codes. |
| src/configuration.rs | Adds per_install_billing_enabled global kill switch (default false). |
| src/cli/stacker_client.rs | Threads install idempotency key + adds response authorization summary + creator_name field. |
| src/cli/service_catalog.rs | Adds catalog inputs + secret placeholders, plus healthchecks + tests. |
| src/cli/install_runner.rs | Ensures docker_registry key is always sent; updates default server size + tests. |
| src/cli/generator/compose.rs | Avoids synthesizing phantom app service; updates runtime labels to use project code; adds tests. |
| src/cli/config_parser.rs | Tracks whether app: was explicitly present (app_present) and updates defaults/tests. |
| src/cli/config_bundle.rs | Adds optional injection of shared agent network into all compose services + tests. |
| src/cli/compose_service_sync.rs | Adds inject_shared_network_all_services helper + tests. |
| src/cli/ai_scenarios.rs | Updates default cloud size used in tests. |
| src/bin/stacker.rs | Adds marketplace install flags for cloud key/region/size/provider. |
| migrations/20260718130000_marketplace_install_authorization.up.sql | Creates marketplace_install_authorization table + indexes. |
| migrations/20260718130000_marketplace_install_authorization.down.sql | Drops marketplace_install_authorization table. |
Comment on lines
+331
to
+379
| // Per-install billing: capture the previously-authorized charge now that | ||
| // the deploy has confirmed successful. Lookup is by deployment_hash on the | ||
| // authorization row (populated at install time); non-per_install rows | ||
| // simply won't match and this becomes a no-op. Non-fatal on capture | ||
| // failure — the sweeper reconciles stragglers. | ||
| match db::marketplace_billing::find_by_deployment_hash( | ||
| pg_pool.get_ref(), | ||
| &payload.deployment_hash, | ||
| ) | ||
| .await | ||
| { | ||
| Ok(Some(auth_row)) if auth_row.status == "authorized" => { | ||
| let service_token = std::env::var("STACKER_SERVICE_TOKEN").unwrap_or_default(); | ||
| match user_service | ||
| .capture_install_charge( | ||
| &service_token, | ||
| &auth_row.authorization_id, | ||
| &payload.deployment_hash, | ||
| ) | ||
| .await | ||
| { | ||
| Ok(_handle) => { | ||
| if let Err(err) = db::marketplace_billing::mark_captured( | ||
| pg_pool.get_ref(), | ||
| &auth_row.authorization_id, | ||
| ) | ||
| .await | ||
| { | ||
| tracing::warn!( | ||
| "mark_captured DB write failed for {}: {}", | ||
| auth_row.authorization_id, | ||
| err | ||
| ); | ||
| } | ||
| } | ||
| Err(err) => tracing::warn!( | ||
| "capture_install_charge failed for {} (deploy already succeeded, sweeper will retry): {}", | ||
| auth_row.authorization_id, | ||
| err | ||
| ), | ||
| } | ||
| } | ||
| Ok(_) => {} // no per_install authorization for this deployment | ||
| Err(err) => tracing::warn!( | ||
| "find_by_deployment_hash failed for {}: {}", | ||
| payload.deployment_hash, | ||
| err | ||
| ), | ||
| } |
Comment on lines
+68
to
+73
| let service_token = std::env::var("STACKER_SERVICE_TOKEN").unwrap_or_default(); | ||
| for row in expired { | ||
| match user_service | ||
| .void_install_charge(&service_token, &row.authorization_id, "expired") | ||
| .await | ||
| { |
Comment on lines
+1942
to
+1947
| t.price = Some(10.005); | ||
| assert_eq!( | ||
| amount_minor_for(&t).unwrap(), | ||
| 1001, | ||
| "banker rounding to nearest cent" | ||
| ); |
Comment on lines
+509
to
+513
| let reason = if is_stack { | ||
| "none of its member apps resolved a container image (each member app needs its own dockerhub_image)" | ||
| } else { | ||
| "the catalog entry is missing a dockerhub_image" | ||
| }; |
Comment on lines
+120
to
+142
| let is_per_install = template | ||
| .billing_cycle | ||
| .as_deref() | ||
| .map(|c| c.trim().eq_ignore_ascii_case("per_install")) | ||
| .unwrap_or(false); | ||
|
|
||
| // Per-install templates skip the ownership check entirely — there is no | ||
| // permanent purchase for these. Instead we probe the user's payment | ||
| // capability so `stacker install` fails fast with a clear 402 if the | ||
| // user has no card on file, rather than deep inside the install handler | ||
| // after the actual authorize attempt. | ||
| if is_per_install { | ||
| let capability = user_service | ||
| .can_charge(user_token) | ||
| .await | ||
| .map_err(validation_failed)?; | ||
| if !capability.can_charge { | ||
| return Err(MarketplaceAccessError::NoPaymentMethod { | ||
| reason: capability.reason.unwrap_or_else(|| "unknown".to_string()), | ||
| }); | ||
| } | ||
| return Ok(()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces several significant improvements to the stacker CLI and configuration system, focusing on better handling of Docker Compose networks for agent deployments, more accurate synthesis of the
appservice in Compose files, and enhanced support for marketplace installs. The changes also improve test coverage and add new migration files for marketplace authorization tracking.Docker Compose network handling:
inject_shared_network_all_servicestocompose_service_sync.rs, ensuring all services in a Compose file join a shared external network (e.g.,default_network), which is critical for agent-based deployments. This function is idempotent and covered by a new unit test. [1] [2]build_config_bundleto optionally attach the agent network to all services when requested, with corresponding tests to verify this behavior. [1] [2] [3]Marketplace install command enhancements:
marketplace installCLI command to accept additional arguments for cloud credential selection, region, size, and provider, improving flexibility for deployments. [1] [2]Compose/app service synthesis improvements:
app_presentflag toStackerConfigto distinguish between configs that explicitly declare anapp:section and those where it is defaulted. The Compose generator now only materializes theappservice when appropriate, preventing phantom services and related deploy errors. [1] [2] [3] [4] [5] [6] [7] [8] [9]appservice's runtime label now uses the project code, ensuring the agent can resolve containers correctly.Database migrations:
marketplace_install_authorizationtable, with appropriate indexes and constraints for tracking authorizations. [1] [2]Test and default value updates:
cpx11tocx23for consistency. [1] [2]These changes collectively improve deployment reliability, configuration accuracy, and user experience for both agent-based and marketplace-driven workflows.