Feature Specification: Deployment lifecycle

Created: 2026-09-23

Status: Implemented

Input: Reconstructed from the code of panel/ at 0bf4ecd36, api/specification.md, pull requests #897, #969, #1011, #1036, #1052, #1186, #1311, #1339, #1777, #1839 and #1842, and the design notes of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - Keep a draft and deploy it (Priority: P1)

An operator makes a deployment, edits its configuration, and saves it as a draft, which can be incomplete. To deploy, the browser validates the draft, saves it, and submits it to api. The page follows the build until it ends. The list shows the state, the domain and the last build of each deployment, and changes live through the event stream of api. The operator can copy a deployment to start a new one.

Why this priority: this is the main task of the panel.

Independent Test: a VM test where an operator makes a deployment, deploys it and sees a final state, and unit tests of the list with an api that does not answer.

Acceptance Scenarios:

  1. Given a new deployment, When the operator makes it, Then it is "Untitled", has no configuration, and opens in the form.
  2. Given a draft, When the operator saves it, Then the panel stores it and confirms the save. A save from a script gets a success answer, or a refusal with the reason.
  3. Given a build that fails, When the page gets the final state, Then the page shows the last lines of the build log as plain text, and the operator can deploy again.
  4. Given a full build queue at api, When the operator deploys, Then the page says that the queue is full.
  5. Given a running build, When api publishes a new state, Then the row in the list changes without a reload.

User Story 2 - Delete a deployment and its infrastructure (Priority: P1)

The operator confirms the delete. The browser asks api to destroy the infrastructure, waits until the destroy ends, and then tells the panel to remove the record. If the browser stops before the end, api calls back the panel with a reconcile token when the destroy is done. To remove the record, the panel marks it deleted: the operator does not see it any more, but the row stays, so that the provider can bill the deployment with usage_list.

Why this priority: a delete that removes the record but not the infrastructure is a leak that nobody sees.

Independent Test: unit tests of the delete flow in the panel and in the browser script.

Acceptance Scenarios:

  1. Given a destroy that succeeds, When the browser sees the end, Then the panel removes the record.
  2. Given a destroy that fails, When the browser sees the failure, Then the record stays, and the operator can try again.
  3. Given a deployment that api does not know, When the operator deletes it, Then the panel removes the record.
  4. Given that the browser closed during a destroy, When api calls back with the correct token, Then the panel removes the record.

User Story 3 - Do an action of api on a deployment (Priority: P2)

The operator suspends or resumes a deployment, or does another action that api offers for the current state ("Deployment actions" in api/docs/design/deployment-actions.md). A new action of api needs no change in the panel.

Independent Test: unit tests of the deployment form, of the form of one application, and of the action script.

Acceptance Scenarios:

  1. Given a deployment that api can suspend, When the operator opens the deployment form, Then the form shows a Suspend button with the description of api as its tooltip.
  2. Given that the operator accepts the confirmation, When the action succeeds, Then the page reloads and shows the actions of the new state.
  3. Given a suspended deployment, When the operator opens the deployment form or the form of one application, Then Deploy is disabled and the page shows why. Only the deployment form shows Resume.

User Story 4 - Choose how a deployment updates (Priority: P2)

A deployment without a module source pin deploys the default source of api, so a new revision of api brings an update, which a deploy applies. The update policy of a deployment says who starts that deploy: the operator (manual), api when it finds the update (automatic), or api in a maintenance window. api stores the policy ("Update policy" in api/docs/design/update-policy.md). The account page holds the defaults of the operator: the policy of a deployment that api does not know yet, the default of the "Apply the available update" check box beside Deploy, and the update notices.

Independent Test: unit tests of the update settings and controls.

Acceptance Scenarios:

  1. Given a new operator, When the operator opens the account page, Then the default policy is manual, the check box is ticked by default, and the notices are off.
  2. Given an available update, When the operator unticks the check box and deploys, Then api deploys the configuration with the source of the last deploy.
  3. Given a deployment that api knows, When the operator saves a maintenance window, Then api stores it and starts the update in the window.
  4. Given that the notices are on, When the operator opens a page, Then a banner lists the deployments with an available update and the updates that api started itself. When the operator closes the second list, it stays hidden until api starts a new update.

Edge Cases

  • The event stream stops during a deploy: the page shows a warning, not a failure, and asks for the state at intervals.
  • The browser gets events of an earlier build: the page acts only on the events of its own build.
  • api does not know a deployment yet: the panel reports it as building and not yet known. The form shows the default policy of the operator, and the first deploy stores it.
  • The check box is unticked and the default source differs from the deployed source: Deploy stays enabled, and a deploy builds the deployed configuration again.

Requirements (mandatory)

Functional Requirements

  • FR-001: The panel MUST store the draft configuration, the name, the owner and the module source pin of a deployment, and MUST accept an incomplete draft. The form MUST NOT show the pin, and a save MUST NOT change it.

  • FR-002: The browser MUST send the deploy and the delete to api directly, through the public HTTP interface of api, with a bearer token from the panel. A deploy MUST send the configuration, the deployment mode and the module source.

  • FR-003: The panel MUST get the state, the domain and the builds of a deployment from api, and MUST NOT store them.

  • FR-004: The panel MUST remove the record of a deployment only after api reports that the destroy is done, or that it does not know the deployment.

  • FR-005: The panel MUST accept a call back about a destroy only with a reconcile token that the panel made for that deployment.

  • FR-006: The panel MUST show an unknown state and a warning, not an error page, when api does not answer.

  • FR-007: A duplicate MUST copy the name, the configuration and the module source pin into a new deployment.

  • FR-008: The deployment form MUST show a control for each action that api offers for the deployment, other than deploy, export and delete. It MUST disable Deploy while api refuses a deploy.

  • FR-009: The list and the deployment form MUST mark each deployment for which api reports an available update, for each update policy.

  • FR-010: A deploy MUST tell api not to apply the available update when the operator unticks the check box. The check box MUST be disabled when no update is available.

  • FR-011: The panel MUST NOT offer a policy other than manual for a deployment with a module source pin, because such a deployment gets no update.

  • FR-012: The panel MUST show the update banner only to an operator who turns on the notices.

  • FR-013: The panel MUST keep the row of a removed record until the removal of the operator account, and MUST answer usage_list and domain_list with the deleted deployments of the operator too. A domain_claim MUST be able to take the domain of a deleted deployment of the operator. All other procedures MUST NOT see a deleted deployment.

  • FR-014: The page of a deployment that api deployed MUST link to the SBOM of each of its nodes. The panel MUST show the status of each node (ready, or unavailable with the reason), with the build that gave it and the time, and MUST serve a ready SBOM as a file download. The panel MUST NOT store the SBOMs. The panel MUST answer 404 for a deployment of another operator without a call to api.

  • FR-015: When the stored configuration of a deployment registers a domain at the registrar account of the provider, the deployment form MUST show the status of the domain from domain_list. When the domain is assignable, the form MUST offer to give the domain to the deployment with domain_claim. The panel MUST show the reason when api refuses the claim. The domains page MUST give a superuser a form that gives a deployment of each operator the domain of its stored configuration, with domain_assign, and the panel MUST show the reason when api refuses.

  • FR-016: When the deployment of the page has a domain with domain terms, the domain card MUST show the documents of the terms with their dates from domain_list, the current version, and the accepted version. When the deployment did not accept the current version, the card MUST tell that the acceptance is pending and MUST offer domain_terms_accept for the version that the page shows. The acceptance MUST NOT start a deploy. The card MUST tell that the acceptance also applies to the other deployments of the operator that need only these terms.

  • FR-017: For each domain of a deployment of an operator with the current version of the domain terms not accepted, the panel MUST send one email per version to the operator, and MUST link to the deployment page in that email. The panel MUST tell api of each email with domain_terms_notice after it sends the email, so that the audit chain has the notice and the next run does not send it again. The panel MUST NOT tell api of an operator with no email address, and MUST try that operator again on the next run. The panel MUST make this call as the service, with the operator as X-On-Behalf-Of, and api MUST refuse the call from each other caller.

  • FR-018: When the deployment of the page holds a domain that has a registration of its own, and domain_list gives renewal_off, the domain card MUST offer domain_renewal_off to turn off a renewal that is on, and MUST tell that this starts a deploy and that the registration ends at its expiry. When renewal_off is true, the card MUST offer domain_renewal_curated to use the renewal of the hosting provider again. The card MUST NOT offer to turn on a renewal that the hosting provider turned off.

  • FR-019: The panel MUST have a domains page that lists the domains of the deployments of the operator from domain_list, also of the deleted deployments, with ?id= always in the call, so a superuser sees only their own domains. For a domain that a deployment of the operator has, the page MUST link to the deployment page. For a domain with the status assignable, the page MUST offer released_domain_renewal_off, released_domain_renewal_curated and domain_take_out, and domain_take_out_cancel when transfer_pending is true. The answer of domain_take_out MUST show the auth code directly, and the panel MUST NOT store the code or put it in the session.

  • FR-020: For a domain where domain_list gives transfer_in, the domains page MUST have a form that sends the auth code to domain_transfer_in for that deployment. The form field MUST NOT show the code, and the panel MUST NOT keep the code. After the call, the page MUST tell the operator that the deploy fails until the registry completes the transfer, and that they deploy again then. When domain_list gives transfer_failed, the page MUST tell the operator that the registry refused the transfer, and MUST NOT offer the actions of a held domain. With transfer_in, the form MUST tell the operator to check the auth code and the transfer lock, and that the code sends the transfer again.

  • FR-021: The deploy controls MUST have a field for the auth code of the domain, which the page sends to deployment_submit as domain_auth_code, so an operator can transfer a domain in at the first deploy of a deployment. The field MUST NOT show the code, and the panel MUST NOT keep the code. The field MUST be empty after a submit that api accepts. The label of the field MUST tell that the deploy fails until the registry completes the transfer, and that the operator then deploys again.

  • FR-022: After domain_terms_accept succeeds, the panel MUST call domain_terms_accept with the token of the operator for each other deployment of that operator with pending domain terms, when the parts of the version that the deployment did not accept are all parts of the accepted version. The call MUST use the current version of the domain of that deployment. The panel MUST NOT store an acceptance. The page MUST tell the operator for which domains the acceptance applies, and for which it does not apply. A failure on a different deployment MUST NOT change the first acceptance.

  • FR-023: The deployment page and a public page MUST show what the hosting provider declares in the provider option of the panel: the legal entity, the data location and the sub-processors. The public page MUST NOT need an account. Both pages MUST show a provider that declares nothing as "Not declared".

  • FR-024: When the operator has a saved domain contact whose handle is not owner_handle of domain_list, the domains page MUST have a form that selects one of these contacts as the owner contact of the deployment and sends the domain to domain_owner_change, for a domain with a registration of its own whose status is held or assignable and whose holder is a deployment of the operator. The form MUST NOT send a handle. When api/ does not change the owner, the panel MUST keep the earlier owner contact of the deployment. The page MUST tell that the owner is the registrant and that the registry can lock the transfer of the domain after the change. The page MUST NOT have the form while transfer_pending is true or the domain has transfer_in. When domain_terms of the domain has a version that is not its accepted, the form MUST link the documents and MUST have a required box that sends that version as accepted_domain_terms.

  • FR-025: An operator MUST be able to save zero or more domain contacts, each with a label, and MUST be able to select for each deployment one saved contact per role: owner, admin, tech and billing. A role other than owner without a contact takes the owner contact. For deployment_submit, migration_import and domain_owner_change on a deployment of the operator, the authorization callback MUST give domain_contacts, a map from each role to the ID of its contact, or an empty string for a role without a saved contact. The callback MUST NOT write a row. api refuses to register or transfer a domain without a saved owner contact, thus a panel that sends no ID cannot make the hosting provider the owner. A new deployment of an operator with one saved contact MUST have that contact as its owner contact.

  • FR-026: Before the first save of a domain contact, the domain contact form MUST have the first name, the last name and the e-mail address of the account, and MUST tell the operator that these come from the account and that the registrar needs real data.

Key Entities

  • Deployment: identifier, owner, name, draft configuration, module source pin, time of creation, time of the delete.
  • Operator settings: the default update policy, the default of the check box, the notice switch, and the time when the operator last closed the notice of the automatic updates.
  • Reconcile token: a signed token for one deployment, with which api confirms a destroy.
  • Build state: active, building, deploying, idle, failed, destroying, or unknown when api does not answer.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: A delete never removes the record of a deployment whose infrastructure api still has.
  • SC-002: The list stays usable when api is down.

Assumptions

  • api queues a destroy when it gets a delete, and does not destroy at once.
  • Save and submit are two steps. A submit that fails after a save leaves a saved draft.

Known Deviations

None.