#!/usr/bin/env bash set -euo pipefail run_id="" repo_url="https://github.com/pikasTech/unidesk" desired_ref="master" manifest_commit="" environment="dev" result_dir="" timeout_ms="1800000" keep_namespace="false" manifest_file="" usage() { cat <<'EOF' Usage: dev-e2e.sh --run-id ID --manifest-commit COMMIT --manifest-file FILE --result-dir DIR [--repo-url URL] [--desired-ref master] [--environment dev] [--timeout-ms MS] [--keep-namespace] This script runs the D601 dev namespace e2e harness from a Git-controlled blob. It must be launched by the CLI with a short command; do not paste this script body through the maintenance channel. EOF } while [ "$#" -gt 0 ]; do case "$1" in --run-id) run_id="${2:-}" shift 2 ;; --repo-url) repo_url="${2:-}" shift 2 ;; --desired-ref) desired_ref="${2:-}" shift 2 ;; --manifest-commit) manifest_commit="${2:-}" shift 2 ;; --environment) environment="${2:-}" shift 2 ;; --result-dir) result_dir="${2:-}" shift 2 ;; --manifest-file) manifest_file="${2:-}" shift 2 ;; --timeout-ms) timeout_ms="${2:-}" shift 2 ;; --keep-namespace) keep_namespace="true" shift ;; -h|--help) usage exit 0 ;; *) echo "unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done if ! [[ "$run_id" =~ ^[a-z0-9]([-a-z0-9]{0,46}[a-z0-9])?$ ]]; then echo "invalid --run-id: $run_id" >&2 exit 2 fi if ! [[ "$manifest_commit" =~ ^[0-9a-f]{40}$ ]]; then echo "--manifest-commit must be a full 40 character SHA" >&2 exit 2 fi if [ "$environment" != "dev" ]; then echo "only --environment dev is supported" >&2 exit 2 fi if ! [[ "$timeout_ms" =~ ^[0-9]+$ ]] || [ "$timeout_ms" -le 0 ]; then echo "--timeout-ms must be a positive integer" >&2 exit 2 fi if [ -z "$result_dir" ]; then result_dir="/home/ubuntu/.unidesk/runs/$run_id" fi if [ -z "$manifest_file" ] || [ ! -f "$manifest_file" ]; then echo "--manifest-file must point to the commit-pinned deploy.json fetched by the launcher" >&2 exit 2 fi mkdir -p "$result_dir" runner_log="$result_dir/runner.log" result_json="$result_dir/result.json" exec > >(tee -a "$runner_log") 2>&1 log_json() { local event="$1" shift || true printf '{"at":"%s","event":"%s"' "$(date -Iseconds)" "$event" while [ "$#" -gt 1 ]; do printf ',"%s":%s' "$1" "$(printf '%s' "$2" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')" shift 2 done printf '}\n' } write_result() { local ok="$1" local status="$2" local detail="$3" python3 - "$result_json" "$ok" "$status" "$detail" "$run_id" "$repo_url" "$desired_ref" "$manifest_commit" "$environment" "$pipeline_run" "$temporary_namespace" "$code_queue_image" <<'PY' import json import sys from datetime import datetime, timezone path, ok, status, detail, run_id, repo, desired_ref, commit, environment, pipeline_run, temporary_namespace, code_queue_image = sys.argv[1:] record = { "ok": ok == "true", "status": status, "detail": detail, "runId": run_id, "repoUrl": repo, "desiredRef": desired_ref, "manifestCommit": commit, "environment": environment, "pipelineRun": pipeline_run or None, "temporaryNamespace": temporary_namespace or None, "codeQueueImage": code_queue_image or None, "finishedAt": datetime.now(timezone.utc).isoformat(), } with open(path, "w", encoding="utf-8") as handle: json.dump(record, handle, ensure_ascii=False, indent=2) handle.write("\n") print(json.dumps(record, ensure_ascii=False)) PY } pipeline_run="" temporary_namespace="unidesk-ci-e2e-$run_id" code_queue_image="" trap 'code=$?; if [ "$code" -ne 0 ] && [ ! -f "$result_json" ]; then write_result false failed "runner exited with code $code" || true; fi' EXIT export KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes >/dev/null log_json runner_started run_id "$run_id" manifest_commit "$manifest_commit" kubectl get pipeline/unidesk-dev-namespace-e2e -n unidesk-ci >/dev/null kubectl get pvc/unidesk-ci-cache -n unidesk-ci >/dev/null service_env="$result_dir/dev-e2e-service-commits.env" python3 - "$manifest_file" "$service_env" "$environment" <<'PY' import json import re import sys manifest_path, out_path, environment = sys.argv[1:] with open(manifest_path, "r", encoding="utf-8") as handle: manifest = json.load(handle) if manifest.get("schemaVersion") != 2: raise SystemExit("deploy.json must use schemaVersion=2") env = manifest.get("environments", {}).get(environment) if not isinstance(env, dict): raise SystemExit(f"deploy.json must contain environments.{environment}") services = env.get("services") if not isinstance(services, list) or not services: raise SystemExit(f"deploy.json environments.{environment}.services must contain services") lines = [] summary = [] for service in services: if not isinstance(service, dict) or not service.get("id") or not service.get("repo") or not service.get("commitId"): raise SystemExit(f"each deploy.json environments.{environment} service must contain id, repo and commitId") key = re.sub(r"[^A-Z0-9]", "_", str(service["id"]).upper()) commit = str(service["commitId"]) lines.append(f"{key}_COMMIT={commit}") summary.append({"id": service["id"], "commitId": commit}) with open(out_path, "w", encoding="utf-8") as handle: handle.write("\n".join(lines) + "\n") print(json.dumps({"ok": True, "environment": environment, "services": summary}, ensure_ascii=False)) PY # shellcheck disable=SC1090 source "$service_env" backend_commit="${BACKEND_CORE_COMMIT:-unknown}" frontend_commit="${FRONTEND_COMMIT:-unknown}" code_queue_commit="${CODE_QUEUE_COMMIT:-unknown}" if ! [[ "$code_queue_commit" =~ ^[0-9a-f]{40}$ ]]; then echo "deploy.json environments.$environment.services must include code-queue with a full 40 character commitId; got: $code_queue_commit" >&2 exit 2 fi deploy_json_b64="$(base64 -w0 "$manifest_file")" work_dir="$(dirname "$manifest_file")" repo_dir="$work_dir/repo" if [ ! -d "$repo_dir/.git" ]; then echo "launcher repo checkout is missing: $repo_dir" >&2 exit 2 fi root_exec() { if [ "$(id -u)" = "0" ]; then "$@" return fi if sudo -n true >/dev/null 2>&1; then sudo -n "$@" return fi if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- "$@" return fi echo "dev_e2e_native_k3s_root_access=missing" >&2 return 1 } import_image_to_k3s() { local image="$1" local archive="/tmp/unidesk-ci-image-${run_id}-${image//[^A-Za-z0-9_.-]/-}.tar" rm -f "$archive" docker save "$image" -o "$archive" root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import --digests --all-platforms "$archive" rm -f "$archive" echo "dev_e2e_image_imported=$image" } ensure_runtime_image() { local image="$1" if ! docker image inspect "$image" >/dev/null 2>&1; then echo "dev_e2e_runtime_image_pull=$image" docker pull --platform linux/amd64 "$image" else echo "dev_e2e_runtime_image_cached=$image" fi import_image_to_k3s "$image" } build_code_queue_image() { local short="${code_queue_commit:0:12}" local commit_image="unidesk-code-queue:ci-$short" local run_image="unidesk-code-queue:ci-dev-e2e-$run_id" local source_dir="$work_dir/code-queue-src-$short" local resolved git -C "$repo_dir" fetch --no-tags origin "$code_queue_commit" || git -C "$repo_dir" fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*' resolved="$(git -C "$repo_dir" rev-parse --verify "$code_queue_commit^{commit}")" if [ "$resolved" != "$code_queue_commit" ]; then echo "code_queue_commit_mismatch resolved=$resolved expected=$code_queue_commit" >&2 exit 1 fi local existing_rev existing_rev="$(docker image inspect "$commit_image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' 2>/dev/null || true)" if [ "$existing_rev" != "$resolved" ]; then rm -rf "$source_dir" mkdir -p "$source_dir" git -C "$repo_dir" archive "$resolved" | tar -x -C "$source_dir" local base_args=() if docker image inspect unidesk-code-queue:d601-build-base >/dev/null 2>&1; then base_args=(--build-arg CODE_QUEUE_BASE_IMAGE=unidesk-code-queue:d601-build-base) elif docker image inspect unidesk-code-queue:d601 >/dev/null 2>&1; then base_args=(--build-arg CODE_QUEUE_BASE_IMAGE=unidesk-code-queue:d601) fi echo "dev_e2e_code_queue_image_build=$commit_image commit=$resolved" docker build \ "${base_args[@]}" \ --build-arg HTTP_PROXY="${HTTP_PROXY:-}" \ --build-arg HTTPS_PROXY="${HTTPS_PROXY:-}" \ --build-arg ALL_PROXY="${ALL_PROXY:-}" \ --build-arg NO_PROXY="${NO_PROXY:-}" \ --label "org.opencontainers.image.source=$repo_url" \ --label "org.opencontainers.image.revision=$resolved" \ --label "unidesk.ai/ci-run-id=$run_id" \ --label "unidesk.ai/ci-kind=dev-e2e" \ -t "$commit_image" \ -f "$source_dir/src/components/microservices/code-queue/Dockerfile" \ "$source_dir" else echo "dev_e2e_code_queue_image_cached=$commit_image commit=$resolved" fi docker tag "$commit_image" "$run_image" import_image_to_k3s "$run_image" code_queue_image="$run_image" } ensure_runtime_image "postgres:16-alpine" build_code_queue_image pipeline_manifest="$result_dir/pipelinerun.yaml" cat >"$pipeline_manifest" <"$result_dir/pipelinerun.txt" log_json pipelinerun_created pipeline_run "$pipeline_run" namespace unidesk-ci deadline=$((SECONDS + (timeout_ms + 999) / 1000)) condition="" while [ "$SECONDS" -lt "$deadline" ]; do condition="$(kubectl get "pipelinerun/$pipeline_run" -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type=="Succeeded")]}{.status}{"\t"}{.reason}{"\t"}{.message}{end}' 2>/dev/null || true)" case "$condition" in True*) kubectl get "pipelinerun/$pipeline_run" -n unidesk-ci -o json >"$result_dir/pipelinerun.json" kubectl get taskrun -n unidesk-ci -l "tekton.dev/pipelineRun=$pipeline_run" -o json >"$result_dir/taskruns.json" || true kubectl logs -n unidesk-ci -l "tekton.dev/pipelineRun=$pipeline_run" --all-containers=true --tail=-1 >"$result_dir/pods.log" 2>&1 || true write_result true succeeded "$condition" exit 0 ;; False*) kubectl get "pipelinerun/$pipeline_run" -n unidesk-ci -o json >"$result_dir/pipelinerun.json" || true kubectl get taskrun -n unidesk-ci -l "tekton.dev/pipelineRun=$pipeline_run" -o json >"$result_dir/taskruns.json" || true kubectl logs -n unidesk-ci -l "tekton.dev/pipelineRun=$pipeline_run" --all-containers=true --tail=-1 >"$result_dir/pods.log" 2>&1 || true write_result false failed "$condition" exit 1 ;; esac sleep 2 done kubectl get "pipelinerun/$pipeline_run" -n unidesk-ci -o json >"$result_dir/pipelinerun.json" || true kubectl get taskrun -n unidesk-ci -l "tekton.dev/pipelineRun=$pipeline_run" -o json >"$result_dir/taskruns.json" || true kubectl logs -n unidesk-ci -l "tekton.dev/pipelineRun=$pipeline_run" --all-containers=true --tail=-1 >"$result_dir/pods.log" 2>&1 || true write_result false timeout "Timed out waiting for pipelinerun/$pipeline_run" exit 124