# Test2 AppRelease installation and upgrade

This is the standalone Test2 path for changing application metadata without a
full-stack deployment. It has one CI workflow and two operator operations:

```text
.github/workflows/manage-test2-apprelease.yml
install | upgrade
```

For the complete Test2 sequence and the smallest-operation decision table, see
[`test2-standalone-deployment.md`](test2-standalone-deployment.md).

## Boundary

This workflow owns the AppRelease transition:

```text
packaged L1 AppRelease + selected L2 installation input
  → Controlplane applies the complete L1/L2 pack set
  → the sealed L2 shell initializes current navigation once
  → Controlplane publishes one Catalog revision
  → installer prepares the canonical L0 serving state
```

It does not build, select, pull, or replace an L0 serving image. It does not
deploy the L1 AppService, frontend, Integration Service, or Runtime Workers. It does not
refresh the database, accept a patch, run `ApplyAll`, or maintain its own
active-state file.

It never operates the L0 process. It reads the protocol/capability contract
published by L0 upgrade and prepares the one canonical L0 state root. After it
succeeds, run the separate L0 management CI with `operation=activate`. There is
no handoff artifact, activation digest, release pointer, or second state root.

Controlplane pack convergence is offline. The first install may run while the
standalone L0 container is in bootstrap mode. Before an upgrade, stop both the
L0 serving process and the L1 AppService through their separate management CIs.
The AppRelease workflow only verifies that the serving target is quiesced; it
never stops either process itself.

## Operations

Use `install` for the first AppInstallation. It creates the first `PREPARE`
operation at Catalog revision and SecurityEpoch zero. If publication succeeded
but state preparation did not, rerunning the same install re-enters through the
normal same-release upgrade path.

Use `upgrade` when an AppInstallation already exists. It reads the current
AppInstallation ID, Catalog revision, SecurityEpoch, and generations from the
database and submits an `UPGRADE` against those exact values. Selecting the
same L1 AppRelease with changed L2 metadata uses the existing supported
active-release refresh path; it does not invent another installation.

Both operations use the same implementation. The operation name changes only
the Controlplane lifecycle precondition.

## Run

First package the selected L1 source with the `nexuscrm-app-packs` owner
workflow. The controller runs from `main`; its `source` input accepts `main` or
one exact commit and reports an immutable AppRelease reference:

```bash
gh workflow run package-sfcrm-apprelease.yml \
  --repo pingcap-inc/nexuscrm-app-packs \
  --ref main \
  -f source=<main-or-exact-40-character-commit>
```

Then select the matching signed L2 installation input and run:

```bash
gh workflow run manage-test2-apprelease.yml \
  --repo pingcap-inc/nexuscrm-pingcap-pack \
  --ref main \
  -f operation=install \
  -f apprelease_reference='ghcr.io/pingcap-inc/nexuscrm-sfcrm-apprelease@sha256:<l1-digest>' \
  -f l2_installation_reference='ghcr.io/pingcap-inc/nexuscrm-serviceification-bom@sha256:<l2-digest>'
```

For a later application update, first stop L0 and the L1 AppService, then run
`upgrade` with the new exact L1 and L2 references:

```bash
gh workflow run deploy-test2-l0.yml \
  --repo pingcap-inc/nexuscrm-pingcap-pack \
  --ref main \
  -f operation=stop

gh workflow run deploy-test2-l1-application.yml \
  --repo pingcap-inc/nexuscrm-pingcap-pack \
  --ref main \
  -f operation=stop
```

The complete operator sequence is `L0 stop → L1 stop → AppRelease upgrade → L0
activate → L1 deploy`. The final L1 deploy uses the desired exact AppService and
frontend images; reuse the pre-stop references when those images did not change.
These are direct operations with no handoff artifact or additional deployment
state.

The L1 OCI contains only the AppRelease package, its publisher attestation and
verification key, and its source identity. It contains no L2 data. The signed
L2 input supplies the selected overlay, Approval publication, deployment-tools
bundle, and default one-shot Catalog Controlplane image. The installer requires
both references to name the same AppRelease before changing the database. It never
consumes the L2 input's Runtime image selection.

### Independently update the Catalog publisher

When only the Catalog publisher needs a fix, keep the same L1 and L2 references
and pass `-f catalog_source_commit=<exact-40-character-controlplane-commit>` to
the AppRelease workflow. That commit must already have a successful signed
`publish-catalog-reconcile-image` publication in `nexuscrm-controlplane`.

The existing owner verifier checks the image signature, exact source identity,
provenance and SBOM against that commit's owner policy, then freezes the verified
image by digest. The same image is used for compatibility, publisher trust,
Catalog reconciliation and work ownership. A missing or invalid publication
stops before metadata changes; an explicit selection never falls back to the
installation default. Omitting the input preserves the signed default.

This does not republish the L2 installation input, repin Core, replace Runtime
or L1 images, or change the database selection. The existing compatibility
preflight still applies. The run summary records the selected Catalog digest.

## Failure behavior

If Controlplane, Catalog publication, or state preparation fails, L0 remains
stopped. Fix the owning failure and rerun the same operation with the same
signed candidate. Retry reads the database authority and uses the normal
PREPARE or UPGRADE path; there is no recovery state machine or automatic
rollback.

A non-idle installation is not an absent installation. After an interrupted
upgrade, the installer reads the original publication and the pending operation
from Catalog, verifies that its idempotency key still matches the selected
AppRelease, L2 tree and starting revision, and resubmits that same request.
Catalog's existing reconciler owns the phase continuation. A different pending
release, inconsistent authority or ambiguous publication stops the upgrade;
never switch to `install` or delete Catalog state to bypass it. The final
read-back still requires an idle ACTIVE installation before preparing Runtime.

After a successful install or upgrade, activate the unchanged L0 image:

```bash
gh workflow run deploy-test2-l0.yml \
  --repo pingcap-inc/nexuscrm-pingcap-pack \
  --ref main \
  -f operation=activate
```

Then deploy the desired exact L1 AppService and frontend as described in
[Test2 L1 Application deployment](test2-l1-application-deployment.md).

## Permission cutover and retained Integration

Test2 preparation requires the complete nine-role signed installation policy,
including `runtime.permission-authoring-command-lifecycle.v1`, before any
metadata mutation. The deployed L0 Worker digest still comes from L0's selected
release; the installation input does not choose or repin it.

For the existing D3 environment, first follow
[test2-standalone-deployment.md](test2-standalone-deployment.md).
Stop Integration through `manage-test2-integration.yml` before a
metadata upgrade and deploy its selected image after L0 activation. This installer never
operates that process. Runtime workload/signing identities and effective S3
authorities survive preparation; OIDC providers are rendered from the existing
environment through the existing D3 provider helper.

## Current navigation configuration

When the installation grants `runtime.application-navigation.v1`, the stopped
AppRelease controller applies the sealed L2 shell and invokes the selected
Catalog image’s `application-navigation` initializer before publishing Catalog.
Existing navigation is preserved, including an explicitly empty document. Legacy
page routes are translated with the selected L1 `navigation_pages.json`; an
unknown or ambiguous route fails closed. The initializer binds migration to the
observed source digest and owns the transactional revision change.

Navigation uses a bounded workload grant and delegated session scope. It is not
a primary Catalog projection. Its `application_owner_id` comes from the sealed
L2 shell’s `frontendAppId`. Replacing processes does not replay initialization.

## Current navigation admission

Navigation uses the workload grant `runtime.application-navigation.v1`; it is
not a Catalog primary capability. The Runtime application owner comes from
the sealed L2 app-shell `frontendAppId`. When a selected handler also uses
workload RPCs, the renderer carries the BOM grants in
`handlerWorkloadCapabilities`. Select a Catalog image that supports this
composite-handler contract through the existing `catalog_source_commit` input.

The shared app-shell adapter prepares and validates the exact navigation input
with the published initializer before any shell SQL. It also validates an
existing initialized document before changing its shell. New shells use JSON
null until the initializer commits the typed document, before Catalog publication. It translates legacy page routes using the exact L1 page
projection, binds migration to the observed source digest, and preserves
initialized edits and explicit empty lists. Repeated shell deployment updates
the release definition and visibility without overwriting navigation or its
application owner. Unknown legacy targets stop the transition.
