ci(longhaul): add build/deploy/monitor workflows + lint & functional smoke gates#413
ci(longhaul): add build/deploy/monitor workflows + lint & functional smoke gates#413WentingWu666666 wants to merge 15 commits into
Conversation
|
🤖 Auto-triaged by documentdb-triage-tool. Applied: Reasoningcomponent from path globs (ci); effort from diff stats (672+0 LOC, 5 files); LLM failed: Invalid response body while trying to fetch https://api.anthropic.com/v1/messages: Premature close If a label is wrong, remove it manually and ping |
Introduces the GitHub Actions plumbing for the long-haul test driver that landed in PR documentdb#405. Three workflows + two deploy manifests: * .github/workflows/longhaul-image-build.yml Build/push test/longhaul image to GHCR on main push and on demand. Tags every run as :sha-<short> (immutable) plus :main. * .github/workflows/longhaul-deploy.yml Roll an image onto the long-haul AKS cluster. Auto-triggered after a successful image build (pins to :sha-<short>) and via workflow_dispatch for rollbacks. Uses a namespace-scoped kubeconfig in the LONGHAUL_KUBECONFIG secret. * .github/workflows/longhaul-monitor.yaml Hourly health poll: Deployment ready, report ConfigMap fresh (<=2h), test result != FAIL. Auto-upgrade and DocumentDB version publishing are intentionally left out and will land in a separate upgrade PR. * test/longhaul/deploy/deployment.yaml Single-replica Deployment + ConfigMap. Image fields templated (__OWNER__/__IMAGE_TAG__) for the deploy workflow to substitute. Aligned with the post-PR-405 env-var surface (LONGHAUL_DOCUMENTDB_URI, no NUM_VERIFIERS) and the credential secret name documented in test/longhaul/README.md (longhaul-documentdb-credentials with a uri key). * test/longhaul/deploy/rbac.yaml Namespace-scoped ServiceAccount/Role/RoleBinding (pods, dbs, configmaps) plus a ClusterRole for metrics.k8s.io. Splits PR documentdb#348 part 3 of 5. Operator/DocumentDB auto-upgrade plus post-upgrade verification follow in PR-4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
8b658e6 to
db58044
Compare
Self-review found 15 issues across the three workflows + deploy
manifest. Fixed in-place; also added a PR-time lint workflow so
future regressions get caught before merge instead of post-merge
against the live cluster.
Critical:
1. longhaul-image-build: build context was test/longhaul, but the
module's go.mod has `replace` directives for ../shared and
../../operator/src. Set context=. and file=test/longhaul/Dockerfile
so the replace targets are inside the build context.
2. longhaul-monitor: operator version check used
`-l app.kubernetes.io/name=documentdb-operator` + .items[0]; the
chart applies that label to three Deployments (operator, sidecar
injector, wal replica). Target the operator Deployment by name
(env.OPERATOR_DEPLOYMENT=documentdb-operator).
3. longhaul-monitor: missing `packages: read` permission; the GHCR
version lookups (`gh api /orgs/.../packages/...`) silently fall
back to chart defaults without it.
4. longhaul-deploy: header comment claimed kubeconfig is scoped via
rbac.yaml. rbac.yaml is the driver pod's in-cluster SA — orthogonal
to the GHA kubeconfig, which the upgrade job needs cluster-admin
for (helm upgrade in another ns + CRD apply). Rewrote header.
Major:
5. All three workflows: secrets.LONGHAUL_KUBECONFIG was inlined into
`run:` blocks via ${{ secrets.* }}. Switch to env: passthrough so
`$` and backticks in the secret cannot shell-inject.
6. longhaul-monitor: ConfigMap longhaul-versions was applied with
`kubectl create --dry-run | kubectl apply -f -`, which sends a full
body and removes any keys the driver may have added (last-applied,
attempt counters, etc.). Switch to server-side apply with
--field-manager=longhaul-monitor so the monitor co-owns only the
fields it sets.
7. longhaul-deploy: redundant `kubectl set image` + always
rollout-restart after `kubectl apply` which already rolls a pod
when the image changes. Dropped the set-image; gate the restart on
mutable tags only (main / latest / sha-*); pinned semver tags
never need a forced restart.
8. (deferred) workflow_run only fires on default branch — covered by
the new lint-longhaul-workflows.yml which runs on PRs.
9. (verified, not a bug) GHCR documentdb image path matches the
URL-encoded subpackage form used in the operator source
(operator/src/internal/utils/constants.go).
Minor:
10. Pinned runs-on to ubuntu-22.04 across all jobs (was ubuntu-latest
in places) so future runner image roll-forward doesn't surprise us.
11. Added concurrency: groups to all three workflows (cancel-in-progress
true for build/lint, false for deploy/monitor to avoid mid-rollout
cancellation).
12. Added timeout-minutes to every job.
13. longhaul-image-build: replaced
cache-{from,to}=type=registry,ref=...:buildcache (accumulates in
GHCR, no GC) with type=gha so cache cycles automatically.
14. longhaul-monitor: added "Dump driver logs on failure" step that
tails 200 lines from the long-haul pod + describes it whenever any
prior step in check-longhaul fails. Run summaries now contain
enough triage context without a kubectl session.
15. test/longhaul/deploy/deployment.yaml ConfigMap was missing the
tunables LONGHAUL_STEADY_STATE_WAIT, LONGHAUL_REPORT_INTERVAL,
LONGHAUL_RESET_DATA. Added with explicit values (60s, 1h, false)
that match the in-code defaults and commentary about when to
override.
New CI gating:
.github/workflows/lint-longhaul-workflows.yml runs on PRs touching
any of the long-haul workflow files, Dockerfile, deploy manifests, or
the operator chart:
- actionlint (+ embedded shellcheck on run: blocks)
- yamllint (strict)
- kubeconform against k8s 1.30 schemas + a Datree CRD catalog
fallback (skips DocumentDB which we don't ship a CRD schema for)
- helm template smoke against operator/documentdb-helm-chart with a
grep assertion that the rendered output still contains a
Deployment named exactly `documentdb-operator` — protects against
regressions of the fix in (2)
- docker buildx with push:false, mirroring production build exactly
to catch context/replace-directive bugs at PR time
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
- lint-longhaul-workflows.yml: replace deprecated reviewdog input
fail_on_error with fail_level: error.
- Group consecutive $GITHUB_OUTPUT redirects with { ...; } >> file
to satisfy shellcheck SC2129 (image-build, deploy, monitor).
- monitor: bind ready output to a shell variable before the numeric
-lt comparison to satisfy shellcheck SC2170.
- image-build: drop extra spaces after cache-to: colon (yamllint).
- monitor: use two spaces before inline comments (yamllint).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Build chart dependencies (cloudnative-pg) before templating and pass --api-versions cert-manager.io/v1/Certificate so the cert-manager preflight check passes during offline rendering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
The 'Publish desired DocumentDB version' step performs a server-side apply on the longhaul-versions ConfigMap with field manager 'longhaul-monitor'. When the ConfigMap already exists and its fields (e.g. .data.last-updated) are owned by another manager such as 'kubectl-client-side-apply' (from an earlier 'kubectl create'/client-side apply), the SSA fails with a field ownership conflict: error: Apply failed with 1 conflict: conflict with "kubectl-client-side-apply" using v1: .data.last-updated Add --force-conflicts so the monitor, which is the intended authoritative writer of desired-documentdb-version and last-updated, cleanly takes ownership of those fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
✅ End-to-end verification evidenceAll four long-haul workflows have been exercised against a live AKS cluster (
What each run proves
Live cluster state at time of run
Note: the monitor's |
The lint-longhaul-workflows.yml file was missing the standard MIT license header present in the other longhaul workflow files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Strengthen the pre-merge long-haul gate: - Broaden the PR path filter to test/longhaul/** so edits to the driver sources (not just Dockerfile/deploy) trigger the gate. Previously a change under test/longhaul/workload, operations, report, etc. would skip the build-smoke and tests entirely. - Add a go-test job that runs 'go vet ./...' and 'go test ./...' for the driver module. The driver has real unit tests (config, journal, monitor, operations, report) that the image build only compiled, never executed, so logic regressions could slip through. - Add a manifest sanity guard enforcing a 512Mi memory-limit floor on the driver Deployment, and bump the manifest from the 256Mi limit (which OOM-kills the driver) to 1Gi. Schema validation cannot catch this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
The verifier's fetchDocs materialised an entire per-cycle window of documents into a slice before auditing them. Steady-state that window is small, but after a disruption pause the verifier catches up on tens of thousands of docs at once; the slice plus BSON-decode buffers spike the heap and OOM-kill the driver against a hard 256Mi cgroup limit — even though the driver's own bookkeeping is only a few MB. Refactor the audit into an incremental auditor (step/finish) and stream the cursor through it one doc at a time, so verify memory is O(1) per cycle instead of O(window). auditDocs is retained as a thin wrapper so the existing table tests are unchanged. Also surface cursor.Err() so a truncated read is reported instead of being silently mistaken for tail loss. With the streaming scan the driver is genuinely lightweight, so: - restore the Deployment memory limit to 256Mi (request 128Mi), - add GOMEMLIMIT=200MiB as a soft GC ceiling below the hard limit, and - drop the now-unnecessary memory-floor guard from the lint workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
🪶 Follow-up: driver is now genuinely lightweight (256Mi, was 1Gi)While gathering evidence I hit repeated OOMKills of the driver and initially bumped its limit to 1Gi. That was masking a symptom, not fixing the cause — a write-load driver shouldn't need 1Gi. Root-caused and fixed properly: Root cause
Fix (commit
|
Add longhaul-smoke.yml, a PR gate that stands up a real DocumentDB on an
ephemeral kind cluster and runs the actual long-haul driver against it for
a short bounded window. It reuses test/longhaul/deploy/{deployment,rbac}.yaml
unmodified (only patching runtime knobs on the shipped ConfigMap for a
bounded, deterministic run) so manifest/driver regressions fail the PR.
Flow: build driver image -> kind -> cert-manager -> CSI hostpath ->
operator via public OCI chart -> single-instance ClusterIP DocumentDB ->
run driver (LONGHAUL_MAX_DURATION bounded, scale ops disabled) -> assert
container exit 0 AND longhaul-report ConfigMap result == PASS.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Refactor longhaul-smoke.yml to delegate environment bring-up (kind,
cert-manager, CSI, operator install, DocumentDB deploy + readiness) to the
shared .github/actions/setup-test-environment composite action instead of
duplicating that logic inline.
To make the action usable on a fork PR without building operator/database
images, add a 'released-chart' fast path: when released-chart-version is set,
skip the operator/sidecar image download/verify/kind-load steps and omit the
DocumentDB CR image block so the released operator uses its own tested default
(public) documentdb/gateway images. This is fully backward compatible — the
new guards are keyed on released-chart-version, which existing callers
(test-e2e.yml) never set, so their behavior is unchanged.
The smoke job then builds only the lightweight driver image, loads it into the
action's kind cluster, wires the connection secret, applies the real
deploy/{rbac,deployment}.yaml (patching only ConfigMap runtime knobs for a
bounded run), and asserts container exit 0 AND longhaul-report result == PASS.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Rewrite longhaul-smoke.yml to mirror test-e2e.yml: a reusable build job (test-build-and-package.yml) produces operator+sidecar from PR source while documentdb/gateway stay on public images via registry-mode probe, then the smoke job loads those artifacts into kind through the shared setup-test-environment action (use-external-images: false) and runs the real long-haul driver against a real DocumentDB for a bounded window, asserting exit 0 + longhaul-report result=PASS. Revert the earlier setup-test-environment/action.yml changes: the released-chart guards were only needed for fork verification, which is no longer the approach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
The smoke run reached the gateway but the driver's TLS handshake failed with 'x509: certificate signed by unknown authority'. NewFromURI relies solely on URI params for TLS, and the Go mongo driver v2 honors tlsInsecure=true (not tlsAllowInvalidCertificates) to skip verification of the self-signed gateway cert. Switch the URI to tlsInsecure=true and drop the redundant replicaSet param, matching the proven form in test/longhaul/README.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
✅ Long-Haul Smoke Gate — verified green on this PRThe new Run: https://github.com/documentdb/documentdb-kubernetes-operator/actions/runs/28883342342 → Long-Haul Smoke job ✅ Final verdict (Assert PASS step): What this proves (real, not static):
Build stage (registry mode) job results:
This complements the static |
There was a problem hiding this comment.
Pull request overview
This PR adds the CI/CD and Kubernetes plumbing to build, deploy, validate, and continuously monitor the long-haul test driver, alongside a workload verifier change to reduce memory usage by streaming Mongo cursor results.
Changes:
- Add GitHub Actions workflows for long-haul image build, AKS deploy, health monitoring + auto-upgrade, PR-time linting, and a kind-based smoke gate.
- Add long-haul Deployment + RBAC manifests for running the driver on the dedicated cluster.
- Refactor the verifier’s audit path to support O(1) memory scanning via a streaming auditor.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
test/longhaul/workload/verifier.go |
Refactors verification to stream cursor results through an incremental auditor, avoiding buffering large doc windows. |
test/longhaul/deploy/rbac.yaml |
Introduces ServiceAccount/Role(+Binding) and metrics ClusterRole(+Binding) for the long-haul driver. |
test/longhaul/deploy/deployment.yaml |
Adds ConfigMap-driven configuration and a single-replica Deployment for the long-haul driver. |
.github/workflows/longhaul-smoke.yml |
Adds a PR gate that runs the driver end-to-end on kind for a short bounded window. |
.github/workflows/longhaul-monitor.yaml |
Adds an hourly monitor that checks driver health, auto-upgrades the operator via Helm, and publishes the desired DocumentDB version. |
.github/workflows/longhaul-image-build.yml |
Adds a main-branch workflow to build/push the long-haul driver image to GHCR with immutable and rolling tags. |
.github/workflows/longhaul-deploy.yml |
Adds auto-deploy (post-build) and manual deploy/rollback to the AKS long-haul cluster. |
.github/workflows/lint-longhaul-workflows.yml |
Adds PR-time static validation (actionlint/yamllint/kubeconform/helm template), long-haul Go tests, and a no-push image build smoke test. |
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| # ServiceAccount for the long-haul test Job. |
There was a problem hiding this comment.
Good catch — fixed in dd49e83. Comment now reads "ServiceAccount for the long-haul test driver Deployment" (the driver runs as a Deployment, not a Job).
| # one-line image rolls. The driver itself is restart-safe: it persists | ||
| # journal/checkpoint state to PVC and resumes from it. |
There was a problem hiding this comment.
Correct — there is no PVC (the manifest mounts no volumes). Fixed in dd49e83: the comment now says the driver resumes each writer's sequence from the DocumentDB workload collection and checkpoints to the longhaul-report ConfigMap (no PVC).
| # Strategy: Recreate (single-replica stateful driver — never run two | ||
| # pods concurrently against the same PVC). |
There was a problem hiding this comment.
Agreed — reframed in dd49e83: "never run two driver pods concurrently against the same DocumentDB cluster / workload collection" (the Recreate rationale is about avoiding concurrent drivers, not a PVC).
| # Whether to drop the workload collection on startup. MUST stay "false" | ||
| # on the long-haul cluster — restarts of the driver pod (rolling image | ||
| # updates, node drains) need to resume durability state from PVC. |
There was a problem hiding this comment.
Fixed in dd49e83 — the LONGHAUL_RESET_DATA comment now points at the actual persistence model: restarts resume durability state from the existing DocumentDB workload collection and longhaul-report ConfigMap checkpoints, not a PVC.
| containers: | ||
| - name: driver | ||
| image: ghcr.io/__OWNER__/documentdb-kubernetes-operator/longhaul-test:__IMAGE_TAG__ | ||
| imagePullPolicy: IfNotPresent |
There was a problem hiding this comment.
Valid — for the :main rolling tag a node could serve a stale cached image. Fixed in dd49e83: imagePullPolicy: Always. Pull cost is negligible for a single long-lived driver and it guarantees mutable tags are refreshed.
| echo "| Immutable | \`${{ steps.refs.outputs.image }}:sha-${{ steps.refs.outputs.short_sha }}\` |" | ||
| echo "| Rolling | \`${{ steps.refs.outputs.image }}:main\` |" | ||
| echo "" | ||
| echo "Deploy by setting the Job spec image to the immutable tag." |
There was a problem hiding this comment.
Fixed in dd49e83 — step summary now says "Deploy by setting the driver Deployment image to the immutable tag."
| # Long Haul Test Monitor | ||
| # | ||
| # Polls the AKS cluster every hour to: | ||
| # 1. Check health: Job status, report staleness, test result |
There was a problem hiding this comment.
Fixed in dd49e83 — header now reads "Check health: Deployment status, report staleness, test result."
| # performs the upgrade in-band as a load-aware operation, so continuous | ||
| # writers/verifiers can catch any data-integrity issues caused by it. | ||
| # | ||
| # Fails if: Job failed, ConfigMap reports FAIL, report is stale (>2h), or Job not found. |
There was a problem hiding this comment.
Fixed in dd49e83 — the "Fails if" line now describes it accurately: fails if the driver Deployment is unhealthy, the report ConfigMap reports FAIL, the report is stale (>2h), or the report ConfigMap is not found.
| helm upgrade documentdb-operator operator/documentdb-helm-chart \ | ||
| -n "${{ env.OPERATOR_NAMESPACE }}" \ | ||
| --set image.tag="$EXPECTED" \ | ||
| --wait --timeout 5m |
There was a problem hiding this comment.
Confirmed and fixed in dd49e83. charts/ and Chart.lock are gitignored, so the cloudnative-pg dependency isn't vendored — a fresh CI checkout would fail the helm upgrade. Added helm dependency update operator/documentdb-helm-chart immediately before the upgrade.
| - name: Verify cluster access | ||
| run: | | ||
| set -euo pipefail | ||
| kubectl version --client=false --output=yaml | head -20 || true |
There was a problem hiding this comment.
Wouldn't the || true here make this pass even if you couldn't access the cluster?
There was a problem hiding this comment.
Great catch — yes, || true made the gate pass even against an unreachable cluster. Fixed in dd49e83: dropped || true so the step fails fast on a bad/expired kubeconfig before the apply/rollout steps run.
…age pull policy, doc accuracy Addresses review comments on documentdb#413: - deploy: drop `|| true` from the "Verify cluster access" step so it fails fast on an unreachable/expired kubeconfig instead of silently proceeding to apply/rollout (alaye-ms). - monitor: run `helm dependency update` before `helm upgrade`; the chart's cloudnative-pg dependency is gitignored (charts/, Chart.lock), so the upgrade would fail on a fresh CI checkout. - deployment: set imagePullPolicy to Always so mutable tags (e.g. :main) are not silently served from a stale node cache. - deployment/rbac/monitor/image-build: correct leftover "Job"/"PVC" design language — the driver runs as a Deployment (no PVC); it resumes writer sequences from the DocumentDB workload collection and checkpoints to the longhaul-report ConfigMap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
… limits The kubeconform step downloads standard Kubernetes JSON schemas from raw.githubusercontent.com, which intermittently returns HTTP 429 to shared CI runner IPs. Both the initial run and a re-run failed purely on schema downloads (Invalid: 0 in every attempt — no actual manifest violations). Make the gate robust: - Cache the downloaded schemas across runs via actions/cache so steady-state lint performs zero schema downloads. - Pass `-cache` and wrap kubeconform in a retry-with-backoff loop so a cold cache hitting a transient 429 recovers instead of failing; the cache lets each retry re-fetch only the schemas still missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
…gate The shipped driver manifest now uses imagePullPolicy: Always (so the registry-based long-haul deploy always refreshes mutable tags like :main). The kind smoke gate side-loads the locally built longhaul-test:smoke image into the cluster, where it exists on no registry — Always forces a pull that fails with ErrImagePull and the rollout times out. Override the policy to IfNotPresent during the smoke deploy so the side-loaded image is used. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Summary
Builds on PR #405 (long-haul driver core, merged).
Adds the GitHub Actions plumbing + Kubernetes manifests to build, deploy,
monitor, and auto-upgrade the long-haul test driver on the dedicated
AKS cluster, plus two CI gates (static lint + functional smoke) that keep the
long-haul workflows and driver honest on every PR.
What this PR adds
Workflows (
.github/workflows/)longhaul-image-build.ymlmain(paths:test/longhaul/**), workflow_dispatchtest/longhaul/Dockerfile, push to GHCR with:sha-<short>(immutable) +:maintagslonghaul-deploy.yml:sha-<short>), workflow_dispatch (manual rollback)kubectl applyDeployment manifest, set image, wait for rolloutlonghaul-monitor.yamllonghaul-versionsConfigMap; the driver performs the in-band DocumentDB upgrade as a load-aware operation so continuous writers/verifiers can catch any data-integrity regressions.lint-longhaul-workflows.ymltest/longhaul/**+test/shared/**), workflow_dispatchgo vet/go build/go teston the driver module, kubeconform + Helm-template smoke on the manifests. Catches YAML/lint/compile regressions before they reach the cluster.longhaul-smoke.ymltest/longhaul/**,test/shared/**, the build workflow,.github/actions/**), workflow_dispatchtest-build-and-package.ymlto build operator + sidecar from PR source (documentdb/gateway stay on public images via the registry probe), stands up kind + cert-manager + a real DocumentDB through the sharedsetup-test-environmentaction, then runs the actual long-haul driver (realdeploy/*.yamlmanifests, only bounded via ConfigMap overrides) and asserts the driver exits0and writeslonghaul-reportresult=PASS.Manifests (
test/longhaul/deploy/)deployment.yaml— single-replica Deployment + tunable ConfigMap; image fields templated (__OWNER__/__IMAGE_TAG__) for the deploy workflow.rbac.yaml— namespace-scoped ServiceAccount/Role/RoleBinding (pods,documentdb.io/dbs, configmaps) + ClusterRole formetrics.k8s.io.Driver hardening (
test/longhaul/workload/)verifier.go— stream the verification scan (cursor-based) instead of buffering the full result set, keeping the driver comfortably within its256Milimit under sustained load (measured ~35Mi RSS). Prevents the OOM the earlier buffered scan hit as the collection grew.CI gates: how they complement each other
lint-longhaul-workflows.ymllonghaul-smoke.ymlSetup required before the deploy/monitor workflows can run
Cluster admin one-time bootstrap (the deployer ServiceAccount is namespace-scoped by design):
kubectl apply -f test/longhaul/deploy/setup.yaml(already on main via PR test(longhaul): add long-haul test driver core #405; namespace + DocumentDB CR + credentials placeholder)kubectl apply -f test/longhaul/deploy/rbac.yamllonghaul-documentdb-credentialssecret with keyuri(seetest/longhaul/README.md)longhaul-testServiceAccount, store as repo secretLONGHAUL_KUBECONFIGTest plan
Lint Long-Haul Workflows— green on PR (actionlint + yamllint + drivergochecks + manifest schema/Helm smoke).Long-Haul Smoke Gate— verified green end-to-end on this PR: driver exit0,longhaul-reportresultPASS, with the database images reused (not rebuilt) via registry mode. Evidence posted in a PR comment.LONGHAUL_KUBECONFIGsecret provisioning).