ci: run Azure deployment gate against HEAD #567
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Local KIND Deployment | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'deployments/charts/**' | |
| - 'deployments/scripts/**' | |
| - 'deployments/values/**' | |
| - 'test/oetf/**' | |
| - 'test/smoke/**' | |
| - 'test/scenarios/**' | |
| - 'test/workflow/**' | |
| - 'bzl/**' | |
| - 'MODULE.bazel' | |
| - 'src/**' | |
| - '!src/scripts/testbot/**' | |
| - '.github/workflows/oetf-kind.yaml' | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: oetf-kind-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| oetf-kind: | |
| name: oetf:deploy_and_run --env kind --tags kind | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| # No git push from this job — drop the auto-injected token from | |
| # the workspace so artifact uploads (kind-smoke-result, | |
| # bazel-testlogs) can't accidentally include or leak it. | |
| persist-credentials: false | |
| - name: Install kind | |
| run: | | |
| set -euo pipefail | |
| KIND_VERSION=v0.24.0 | |
| KIND_SHA256=b89aada5a39d620da3fcd16435b7f28d858927dd53f92cbac77686b0588b600d | |
| curl -fsSLo /usr/local/bin/kind \ | |
| "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64" | |
| echo "${KIND_SHA256} /usr/local/bin/kind" | sha256sum -c - | |
| sudo chmod +x /usr/local/bin/kind | |
| - name: Install kubectl | |
| run: | | |
| set -euo pipefail | |
| KUBECTL_VERSION=v1.31.0 | |
| curl -fsSLo /usr/local/bin/kubectl \ | |
| "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" | |
| curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256" \ | |
| | awk '{print $1" /usr/local/bin/kubectl"}' | sha256sum -c - | |
| sudo chmod +x /usr/local/bin/kubectl | |
| - name: Install helm | |
| run: | | |
| set -euo pipefail | |
| HELM_VERSION=v3.16.2 | |
| HELM_SHA256=9318379b847e333460d33d291d4c088156299a26cd93d570a7f5d0c36e50b5bb | |
| curl -fsSLo /tmp/helm.tgz \ | |
| "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" | |
| echo "${HELM_SHA256} /tmp/helm.tgz" | sha256sum -c - | |
| tar -xzf /tmp/helm.tgz -C /tmp linux-amd64/helm | |
| sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm | |
| rm -rf /tmp/helm.tgz /tmp/linux-amd64 | |
| - name: Tool versions | |
| run: | | |
| docker --version | |
| kind version | |
| kubectl version --client --output=yaml | head -2 | tail -1 | |
| helm version --short | |
| - name: Setup Bazel | |
| uses: bazel-contrib/setup-bazel@4fd964a13a440a8aeb0be47350db2fc640f19ca8 | |
| with: | |
| bazelisk-cache: true | |
| bazelisk-version: 1.27.0 | |
| disk-cache: ${{ github.workflow }} | |
| repository-cache: true | |
| - name: Add quick-start.osmo to /etc/hosts | |
| run: | | |
| # The KIND adapter's check_kind_prereqs requires this name to | |
| # resolve so tests can reach the chart's ingress at | |
| # http://quick-start.osmo via the host port mapping published by | |
| # `kind create cluster --config`. | |
| echo "127.0.0.1 quick-start.osmo" | sudo tee -a /etc/hosts | |
| - name: Preflight (OETF KIND adapter check_kind_prereqs) | |
| run: | | |
| set -euo pipefail | |
| missing=() | |
| for bin in docker kind kubectl helm; do | |
| command -v "$bin" >/dev/null 2>&1 || missing+=("$bin") | |
| done | |
| if (( ${#missing[@]} > 0 )); then | |
| echo "ERROR: missing on runner: ${missing[*]}" | |
| exit 3 | |
| fi | |
| - name: Free runner disk for --build-local | |
| # Reclaim ~22-25 GB by removing pre-installed tooling the OETF | |
| # gate doesn't use. Pairs with the intra-step host-docker cleanup | |
| # in local_images.build_and_load (docker rmi + tarball delete per | |
| # image after kind-load), needed because the chart's 6-node KIND | |
| # profile duplicates every loaded image into 6 separate | |
| # containerd content stores. Without both mitigations the runner | |
| # crashes mid-job with "No space left on device" (measured #29, | |
| # #31, #32). | |
| run: | | |
| set -euo pipefail | |
| for d in /usr/share/dotnet /usr/local/lib/android /opt/ghc \ | |
| /opt/hostedtoolcache/PyPy /opt/hostedtoolcache/CodeQL \ | |
| /opt/google/chrome /opt/microsoft /opt/az /opt/pipx \ | |
| /opt/swift /usr/local/share/boost \ | |
| /usr/local/share/powershell; do | |
| if [ -d "$d" ]; then | |
| sudo rm -rf "$d" & | |
| fi | |
| done | |
| wait | |
| sudo docker image prune -af >/dev/null 2>&1 || true | |
| - name: Disk space (pre-deploy) | |
| run: df -h / | |
| - name: Build osmo CLI | |
| run: | | |
| # CLI-mode scenarios (e.g. router-connectivity, smoke:cli-checks) | |
| # need an `osmo` binary to invoke. Build from //src/cli and expose | |
| # at /usr/local/bin/osmo (the default --local-osmo path) via | |
| # symlink. | |
| bazel build //src/cli:cli | |
| sudo ln -sf "$(pwd)/bazel-bin/src/cli/cli" /usr/local/bin/osmo | |
| /usr/local/bin/osmo --help >/dev/null | |
| - name: Run OETF deploy_and_run on KIND | |
| id: deploy_and_run | |
| run: | | |
| # --build-local + --use-local-registry: build 9 OSMO service images | |
| # + web-ui from PR source, push to a host-side `registry:2` | |
| # container, KIND nodes pull from it on-demand. Replaces `kind | |
| # load docker-image` which duplicates each image into every node's | |
| # containerd content store (6 nodes × 10 images = 60 image-copies | |
| # — overruns hosted ubuntu-latest's 145 GB disk). With registry | |
| # mode each KIND node only pulls images its pods actually | |
| # schedule (1-2 nodes per image under the chart's node_group | |
| # selectors), dropping the multiplier from 6x to 1-2x. | |
| # | |
| # web-ui IS built and pushed: ingress-nginx has a hard | |
| # `wait-for-web-ui` init container that polls osmo-ui:80, so | |
| # scaling web-ui to 0 deadlocks the chart (verified empirically | |
| # in run #35). Registry-push UI keeps the chart whole at ~1 GB | |
| # host disk vs the prior ~5 GB kind-load duplication. | |
| # | |
| # "Free runner disk" step above reclaims 22-25 GB pre-deploy. | |
| # | |
| # Scenario set narrowed for PR-gate cost: smoke (api-checks + | |
| # websocket-checks, ~5s) covers service+router+logger HTTP/WS; | |
| # templates + mount-validation (~6s) submit workflows so | |
| # backend-listener / backend-worker / osmo_ctrl runtime get | |
| # touched. workflow-labels exercises the label policy gate | |
| # (off/warn/enforce), label-syntax validation, list filters, and a | |
| # labeled round-trip through the API. | |
| # | |
| # --jobs=1 serializes: chart's single-replica ingress-nginx 504s | |
| # under parallel scenario submits. | |
| # | |
| # `time` writes total wall clock to the step log so regressions | |
| # past the ~15-min PR-gate budget show up in the run timeline. | |
| # EXIT trap so the timing block lands on the failure path too. | |
| # | |
| # --keep + --keep-on-failure: leave the cluster intact on either | |
| # deploy or test failure so the "Diagnose KIND state on failure" | |
| # step below can capture pod state. | |
| trap ' | |
| echo "::group::deploy_and_run wall clock" | |
| cat /tmp/oetf.time 2>/dev/null || echo "(no timing file written)" | |
| echo "::endgroup::" | |
| echo "::group::Disk space (post-run)" | |
| df -h / | |
| echo "::endgroup::" | |
| ' EXIT | |
| # Keep this below the job's 60-minute ceiling. Without an inner | |
| # deadline, a stuck Helm/Kubernetes wait consumes the whole job and | |
| # GitHub kills it before the diagnostic and artifact steps can run. | |
| # `timeout` returns 124, which is an ordinary step failure, leaving | |
| # roughly ten minutes for cluster state and logs to be collected. | |
| /usr/bin/time -p -o /tmp/oetf.time \ | |
| timeout --signal=TERM --kill-after=30s 40m \ | |
| bazel run //test/oetf:deploy_and_run -- \ | |
| --env kind --tags kind \ | |
| --target-pattern //test/smoke/... \ | |
| --target-pattern //test/scenarios:templates,//test/scenarios:mount-validation,//test/scenarios:workflow-labels \ | |
| --jobs 1 \ | |
| --build-local --use-local-registry \ | |
| --keep --keep-on-failure \ | |
| --output-json "${GITHUB_WORKSPACE}/kind-smoke-result.json" | |
| - name: Diagnose KIND state on failure | |
| if: failure() && steps.deploy_and_run.outcome == 'failure' | |
| # Triggered only on deploy_and_run failure — preserves the cluster's | |
| # post-mortem state for inspection BEFORE the cleanup step deletes | |
| # it. Each subsection is wrapped in ::group:: so reviewers can fold | |
| # what they don't need. `|| true` everywhere — diagnostic must | |
| # never mask the real failure. | |
| run: | | |
| echo "::group::All pods (kubectl get pods -A -o wide)" | |
| kubectl get pods -A -o wide || true | |
| echo "::endgroup::" | |
| echo "::group::Recent events (last 100, sorted by lastTimestamp)" | |
| kubectl get events -A --sort-by=.lastTimestamp 2>/dev/null | tail -100 || true | |
| echo "::endgroup::" | |
| echo "::group::Non-Ready pod descriptions (osmo + kai-scheduler + ingress-nginx + kube-system)" | |
| for ns in osmo kai-scheduler ingress-nginx kube-system; do | |
| # `kubectl get pods` field-selector can't filter on .status.conditions; do it in jsonpath. | |
| not_ready=$(kubectl get pods -n "$ns" -o jsonpath='{range .items[?(@.status.phase!="Running")]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) | |
| also_unready=$(kubectl get pods -n "$ns" -o jsonpath='{range .items[?(@.status.containerStatuses[*].ready==false)]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) | |
| for pod in $(printf '%s\n%s\n' "$not_ready" "$also_unready" | sort -u); do | |
| [ -z "$pod" ] && continue | |
| echo "--- $ns/$pod ---" | |
| kubectl describe pod -n "$ns" "$pod" 2>/dev/null || true | |
| done | |
| done | |
| echo "::endgroup::" | |
| echo "::group::Non-Ready pod logs (last 200 lines, with --previous on crash)" | |
| for ns in osmo kai-scheduler ingress-nginx; do | |
| not_ready=$(kubectl get pods -n "$ns" -o jsonpath='{range .items[?(@.status.phase!="Running")]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) | |
| also_unready=$(kubectl get pods -n "$ns" -o jsonpath='{range .items[?(@.status.containerStatuses[*].ready==false)]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) | |
| for pod in $(printf '%s\n%s\n' "$not_ready" "$also_unready" | sort -u); do | |
| [ -z "$pod" ] && continue | |
| echo "--- $ns/$pod (current) ---" | |
| kubectl logs -n "$ns" "$pod" --all-containers --tail=200 2>/dev/null || true | |
| echo "--- $ns/$pod (previous, if crash-looped) ---" | |
| kubectl logs -n "$ns" "$pod" --all-containers --previous --tail=200 2>/dev/null || true | |
| done | |
| done | |
| echo "::endgroup::" | |
| echo "::group::All OSMO pod logs (last 80 lines)" | |
| for pod in $(kubectl get pods -n osmo -o name 2>/dev/null || true); do | |
| echo "--- osmo/$pod ---" | |
| kubectl logs -n osmo "$pod" --all-containers --tail=80 2>/dev/null || true | |
| done | |
| echo "::endgroup::" | |
| echo "::group::Helm releases (helm list -A)" | |
| helm list -A || true | |
| echo "::endgroup::" | |
| echo "::group::Disk space" | |
| df -h / | |
| echo "::endgroup::" | |
| - name: Cleanup KIND cluster | |
| if: always() | |
| run: | | |
| kind delete clusters --all || true | |
| - name: Write run summary to GitHub step summary | |
| if: always() | |
| # Surfaces the [PASS]/[FAIL] table on the PR check UI so reviewers | |
| # see results without scrolling the 10-min step log. Reads from | |
| # kind-smoke-result.json (always written by oetf:run, even on test | |
| # failure) and falls back to a deploy-failure note if missing. | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## OETF KIND results" | |
| echo | |
| if [ -s kind-smoke-result.json ]; then | |
| # jq is preinstalled on ubuntu-latest runners. | |
| total=$(jq -r '.total' kind-smoke-result.json) | |
| passed=$(jq -r '.passed' kind-smoke-result.json) | |
| failed=$(jq -r '.failed' kind-smoke-result.json) | |
| errored=$(jq -r '.errored' kind-smoke-result.json) | |
| skipped=$(jq -r '.skipped' kind-smoke-result.json) | |
| echo "**Total ${total} Passed ${passed} Failed ${failed} Errors ${errored} Skipped ${skipped}**" | |
| echo | |
| echo "| Result | Test | Duration |" | |
| echo "| --- | --- | --- |" | |
| # `.target` is the bazel label (e.g. //test/smoke:api-checks); fall back to `.name` for ad-hoc tests. | |
| jq -r '.results[] | "| \(.status|ascii_upcase) | \(if .target != "" then .target else .name end) | \(.time | tonumber | (. * 100 | round / 100))s |"' kind-smoke-result.json | |
| else | |
| echo "_No kind-smoke-result.json — deploy likely failed before tests ran. See \"Diagnose KIND state\" step above._" | |
| fi | |
| echo | |
| if [ -s /tmp/oetf.time ]; then | |
| echo "### Wall clock" | |
| echo '```' | |
| cat /tmp/oetf.time | |
| echo '```' | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload OETF result JSON | |
| if: always() | |
| uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 | |
| with: | |
| name: kind-smoke-result | |
| path: kind-smoke-result.json | |
| if-no-files-found: warn | |
| - name: Upload bazel test logs | |
| if: failure() | |
| uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 | |
| with: | |
| name: bazel-testlogs | |
| path: bazel-testlogs/ | |
| if-no-files-found: warn | |
| # Warm-run trigger (no behavior change). |