# IPZ E2E repo delivery

## Decision

The k3s Job uses the buildbox's local `/home/user/builds` tree as a read-only
`hostPath`. The volume is mounted with a branch-specific `subPath`, and an init
container copies only `plugins/international-press-zone` into a per-Pod
`emptyDir`. The WordPress container mounts that `emptyDir` at
`/var/www/html/wp-content/plugins/international-press-zone`.

This avoids an OCI repository for source delivery and keeps the host mirror
immutable while WordPress runs. The source mirror is node-local, so scheduling
is valid only on a node that has the requested mirror.

The reusable fragment is
[`manifests/repo-delivery.yaml`](../manifests/repo-delivery.yaml). Merge its
`securityContext`, `volumes`, and `initContainers` into the Job's
`spec.template.spec`, and add the documented `ipz-plugin` mount to the
WordPress container.

## Job parameters and rendering

The dispatcher must render the fragment before `kubectl apply`; Kubernetes does
not expand environment variables in `hostPath.path` or `volumeMounts.subPath`.
The renderer must fail before submission if any required value is absent or
invalid.

| Parameter | Required value | Where it is used |
| --- | --- | --- |
| `IPZ_E2E_MIRROR_SLUG` | The branch-local mirror directory name, for example `ipz-ui-makeover-0dfca873ae9b`. It must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`; `/`, `..`, whitespace, and shell metacharacters are forbidden. | Replaces `__IPZ_E2E_MIRROR_SLUG__` in `subPath` and in the init-container environment. |
| `IPZ_E2E_EXPECTED_ASSET_MANIFEST_SHA256` | Lower-case 64-character SHA-256 for the canonical branch's built `admin/dist/asset-manifest.json`. Compute it from the expected branch artifact, not from the node mirror after the node has been selected. | Replaces `__IPZ_E2E_EXPECTED_ASSET_MANIFEST_SHA256__`; the init container compares the mounted file byte-for-byte by digest. |
| `IPZ_E2E_RUNNER_IMAGE` | The digest-pinned image produced by the runner-image task and imported on each eligible node. | Replaces `__IPZ_E2E_RUNNER_IMAGE__`; `IfNotPresent` prevents an unexpected registry pull. |

The source path is therefore:

```text
/home/user/builds/${IPZ_E2E_MIRROR_SLUG}/plugins/international-press-zone/
```

`${IPZ_E2E_MIRROR_SLUG}` above is documentation notation. The submitted YAML
contains the rendered literal in `subPath`; do not pass an unexpanded shell
expression to the Kubernetes API.

## Init-container gate

The init container performs these checks in order:

1. The selected mirror contains the plugin directory.
2. `admin/dist/asset-manifest.json` **MUST exist and be non-empty**.
3. The expected digest is present, is exactly 64 lower-case hexadecimal
   characters, and matches the mounted manifest.
4. Only after all checks pass, the plugin directory is copied into `ipz-plugin`
   and the copied manifest is checked again.

Any failed check exits non-zero. Kubernetes then does not start the WordPress
container, and the Job remains a failed delivery instead of running tests
against an incomplete or unverified plugin. The fragment deliberately uses
`hostPath.type: Directory`, never `DirectoryOrCreate`, so it cannot manufacture
an empty source tree.

The WordPress container must include this mount:

```yaml
volumeMounts:
  - name: ipz-plugin
    mountPath: /var/www/html/wp-content/plugins/international-press-zone
    readOnly: true
```

The init image runs unprivileged and writes the `emptyDir` through the pod
`fsGroup` shown in the fragment. Keep the mirror mount read-only; only the
per-Pod delivery volume is writable.

## Missing and stale mirrors

Because `hostPath` is local to the selected node, the dispatcher must validate
mirror availability on that node or constrain the Job to a node with the
mirror. It must not assume that a slug present on one buildbox exists on the
other two.

| Node state | Observable result | Required action |
| --- | --- | --- |
| `/home/user/builds` is absent | The `Directory` hostPath cannot mount. | Treat the Job as failed delivery; do not create the directory or fall back to another source. |
| The requested slug is absent on the node | The `subPath` mount fails before the init container can copy anything. | Surface the node/mirror mismatch and schedule only on a validated node. |
| The slug exists but the manifest is missing or empty | `deliver-ipz-plugin` exits non-zero at the manifest gate. | Do not start WordPress or run Playwright. |
| The mounted manifest digest differs from the expected digest | `deliver-ipz-plugin` reports a stale/wrong mirror and exits non-zero before copying. | Treat the mirror as stale, refresh/rebuild the branch mirror, or select a node with the expected artifact; never silently accept it. |
| The copy or post-copy check fails | The init container exits non-zero. | Keep the run failed and retain the normal Job/receipt failure handling. |

A node whose mirror is stale therefore fails closed. The digest must be supplied
from the expected branch artifact rather than learned from the mounted mirror;
otherwise a stale mirror could certify itself.

## Scheduling and cleanup contract

- The Job owner is responsible for node selection and for carrying the rendered
  slug alongside the run ID in its labels/parameters.
- The delivery volume is `emptyDir`, so it disappears with the Pod. No plugin
  copy is written back to `/home/user/builds`.
- Job cleanup and artifact extraction remain the responsibility of the Job
  dispatcher. A delivery failure must be classified before any test-result
  artifact is considered usable.
- This strategy does not modify the existing debian2 SSH lane, the buildbox
  mirrors, or the `platform-registry` namespace.
