9f95183c
feat(ship): the pipeline converges the fleet before it asks the smoke to prove it
a73x 2026-08-11 18:41
Commit message
deploy/server/README.md
| Old | New | ||
|---|---|---|---|
| @@ -133,9 +133,16 @@ failure. | |||
| 133 | manifest at boot and pins the answer for 24 hours, so a server rolled ahead | 133 | manifest at boot and pins the answer for 24 hours, so a server rolled ahead |
| 134 | of its site serves the previous release to the whole fleet for a day. | 134 | of its site serves the previous release to the whole fleet for a day. |
| 135 | 7. **Roll the control plane.** Recreate strategy — a brief gap; agents redial. | 135 | 7. **Roll the control plane.** Recreate strategy — a brief gap; agents redial. |
| 136 | 8. **Hosted smoke**: the same `eitri-smoke` the branch gate runs, against this | 136 | 8. **Converge the fleet's agents** on the tag just rolled. Agents never upgrade |
| 137 | themselves, and the smoke below drives a full VM cycle on whichever host the | ||
| 138 | plane picks, so every connected host behind the release is offered the | ||
| 139 | upgrade here and the stage waits (up to 300s) for all of them to report it. | ||
| 140 | A disconnected host is warned about and skipped — it cannot be offered | ||
| 141 | anything and the smoke cannot place a VM on it either. It runs under | ||
| 142 | `--skip-smoke` too: a fleet running the release is part of shipping it. | ||
| 143 | 9. **Hosted smoke**: the same `eitri-smoke` the branch gate runs, against this | ||
| 137 | plane's public names. | 144 | plane's public names. |
| 138 | 9. **Report** what is actually running. | 145 | 10. **Report** what is actually running. |
| 139 | 146 | ||
| 140 | ### A tag publishes once | 147 | ### A tag publishes once |
| 141 | 148 | ||
| @@ -485,7 +492,7 @@ guests exist at the time. | |||
| 485 | 3. `scripts/ship.sh --target stg --tag v0.0.4-pre.1`. Expect stage 4 to fail the | 492 | 3. `scripts/ship.sh --target stg --tag v0.0.4-pre.1`. Expect stage 4 to fail the |
| 486 | first time — that is the mechanism working. The fix is a Secret patch plus a | 493 | first time — that is the mechanism working. The fix is a Secret patch plus a |
| 487 | `config.required` classification, both recorded. | 494 | `config.required` classification, both recorded. |
| 488 | 4. Migrate mewtwo (above) and re-run stage 8. | 495 | 4. Migrate mewtwo (above) and re-run stage 9. |
| 489 | 5. **The proof this run exists to produce**: drive `vm_create` over | 496 | 5. **The proof this run exists to produce**: drive `vm_create` over |
| 490 | `https://stg.eitri.sh/mcp` — the proxied origin — and observe whether progress | 497 | `https://stg.eitri.sh/mcp` — the proxied origin — and observe whether progress |
| 491 | tokens keep Cloudflare from 524-ing a ten-minute call. Record the answer in | 498 | tokens keep Cloudflare from 524-ing a ten-minute call. Record the answer in |
docs/upgrade.md
| Old | New | ||
|---|---|---|---|
| @@ -23,6 +23,11 @@ If a download fails, nothing is swapped; click again to retry. To roll back by | |||
| 23 | hand, stop the agent, move `eitri-agent.prev` back over the binary, and start | 23 | hand, stop the agent, move `eitri-agent.prev` back over the binary, and start |
| 24 | it again. | 24 | it again. |
| 25 | 25 | ||
| 26 | The release pipeline makes this same offer to every connected host after it | ||
| 27 | rolls a plane, and waits for them to report the new version, so a hosted fleet | ||
| 28 | arrives at a release without anyone clicking through it host by host | ||
| 29 | (`deploy/server/README.md`, stage 8). | ||
| 30 | |||
| 26 | **Requirements.** The button lights up only when the running agent's version | 31 | **Requirements.** The button lights up only when the running agent's version |
| 27 | orders before the published release. Release tags (`vX.Y.Z`), pre-releases | 32 | orders before the published release. Release tags (`vX.Y.Z`), pre-releases |
| 28 | (`vX.Y.Z-pre.N`, which order below the release they lead to), and git-describe | 33 | (`vX.Y.Z-pre.N`, which order below the release they lead to), and git-describe |
scripts/ship.env.example
| Old | New | ||
|---|---|---|---|
| @@ -19,6 +19,9 @@ SITE_PLATFORM="linux/arm64" | |||
| 19 | # ── Credentials ─────────────────────────────────────────────────────────────── | 19 | # ── Credentials ─────────────────────────────────────────────────────────────── |
| 20 | # Name ONE of the two arrangements. ship.sh forwards whatever it finds here and | 20 | # Name ONE of the two arrangements. ship.sh forwards whatever it finds here and |
| 21 | # clears the rest, so the plane decides and the pipeline stays the same script. | 21 | # clears the rest, so the plane decides and the pipeline stays the same script. |
| 22 | # The same credential converges the fleet's agents one stage earlier, so the | ||
| 23 | # identity named here must be the one whose tenant owns the fleet's hosts — a | ||
| 24 | # credential from another tenant sees no hosts, and stage 8 says so. | ||
| 22 | # | 25 | # |
| 23 | # (a) A plane running the bundled eitri-oidc — stg. The smoke signs in through | 26 | # (a) A plane running the bundled eitri-oidc — stg. The smoke signs in through |
| 24 | # the real code flow, which proves the whole credential chain, and mints its | 27 | # the real code flow, which proves the whole credential chain, and mints its |
scripts/ship.sh
| Old | New | ||
|---|---|---|---|
| @@ -8,7 +8,8 @@ | |||
| 8 | # scripts/ship.sh --target <stg|prod> --tag <vX.Y.Z[-pre.N]> [options] | 8 | # scripts/ship.sh --target <stg|prod> --tag <vX.Y.Z[-pre.N]> [options] |
| 9 | # | 9 | # |
| 10 | # --from <n> resume at stage n after a partial failure | 10 | # --from <n> resume at stage n after a partial failure |
| 11 | # --skip-smoke stop after the roll (stage 9 still reports) | 11 | # --skip-smoke deploy and converge the fleet, but do not prove it |
| 12 | # (stage 10 still reports) | ||
| 12 | # --render-only print the rendered manifests and exit; touches nothing | 13 | # --render-only print the rendered manifests and exit; touches nothing |
| 13 | # | 14 | # |
| 14 | # Every stage is idempotent: re-running from the top is always safe and is the | 15 | # Every stage is idempotent: re-running from the top is always safe and is the |
| @@ -39,7 +40,7 @@ SKIP_SMOKE=0 | |||
| 39 | RENDER_ONLY=0 | 40 | RENDER_ONLY=0 |
| 40 | 41 | ||
| 41 | usage() { | 42 | usage() { |
| 42 | sed -n '2,32p' "$0" | sed 's/^#\{1,2\} \{0,1\}//' | 43 | sed -n '2,33p' "$0" | sed 's/^#\{1,2\} \{0,1\}//' |
| 43 | exit "${1:-1}" | 44 | exit "${1:-1}" |
| 44 | } | 45 | } |
| 45 | 46 | ||
| @@ -60,7 +61,7 @@ stg | prod) ;; | |||
| 60 | *) echo "ship: --target must be stg or prod (got '${TARGET}')" >&2; usage ;; | 61 | *) echo "ship: --target must be stg or prod (got '${TARGET}')" >&2; usage ;; |
| 61 | esac | 62 | esac |
| 62 | [[ -n "$TAG" ]] || { echo "ship: --tag is required" >&2; usage; } | 63 | [[ -n "$TAG" ]] || { echo "ship: --tag is required" >&2; usage; } |
| 63 | [[ "$FROM" =~ ^[1-9]$ ]] || { echo "ship: --from must be a stage number 1-9" >&2; exit 1; } | 64 | [[ "$FROM" =~ ^([1-9]|10)$ ]] || { echo "ship: --from must be a stage number 1-10" >&2; exit 1; } |
| 64 | 65 | ||
| 65 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | 66 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 66 | cd "$REPO_ROOT" | 67 | cd "$REPO_ROOT" |
| @@ -525,7 +526,278 @@ if [[ "$FROM" -le 7 ]]; then | |||
| 525 | fi | 526 | fi |
| 526 | fi | 527 | fi |
| 527 | 528 | ||
| 528 | # ── 8. Hosted smoke ─────────────────────────────────────────────────────────── | 529 | # ── 8. Converge the fleet's agents ──────────────────────────────────────────── |
| 530 | # | ||
| 531 | # Nothing in eitri upgrades itself on a schedule. An agent takes a new binary | ||
| 532 | # only when an operator offers it one, so a bad release stops at the first host | ||
| 533 | # instead of running through the fleet. That is deliberate, and it has a | ||
| 534 | # consequence for this script: the smoke below drives a whole VM cycle — create, | ||
| 535 | # boot, gate SSH, a published port — on whichever host the plane places it on, | ||
| 536 | # and a leg this release adds does not exist on an agent from the release | ||
| 537 | # before. On v0.0.5 that is what happened, on both planes. The plane served the | ||
| 538 | # new tag, the hosts still ran the old agent, the smoke failed on the leg their | ||
| 539 | # agents did not have, and the release waited while an operator upgraded three | ||
| 540 | # machines by hand and resumed the run. | ||
| 541 | # | ||
| 542 | # So the click lives here now, once per release, between the roll and the proof. | ||
| 543 | # Nothing about an offer changed: it is still per host, still audited, still | ||
| 544 | # refused for a host that is offline or not behind. What changed is that the | ||
| 545 | # pipeline no longer hands the smoke a fleet it already knows is too old. | ||
| 546 | # | ||
| 547 | # The stage runs even under --skip-smoke. A fleet running the release is part of | ||
| 548 | # shipping it, not part of proving it — a plane at $TAG with agents a release | ||
| 549 | # behind is exactly the half-shipped state this stage exists to end. | ||
| 550 | if [[ "$FROM" -le 8 ]]; then | ||
| 551 | bold "8. Converge the fleet's agents to $TAG" | ||
| 552 | |||
| 553 | # How long a host may take to land the new agent before the ship gives up on | ||
| 554 | # it. The work behind an offer is a download, a sha256, a binary swap and a | ||
| 555 | # re-exec, and the offer itself rides the host's next snapshot — one agent | ||
| 556 | # tick, ~10s — so on a healthy link the whole thing is done in tens of | ||
| 557 | # seconds. The console stops calling an outstanding offer "in flight" and | ||
| 558 | # starts calling it stuck at 180s (UPGRADE_STUCK_S, web/src/lib/fleet.svelte.ts), | ||
| 559 | # and a release must not fail before the console would even raise an eyebrow. | ||
| 560 | # 300s is that threshold plus a whole further offer cycle of slack for a slow | ||
| 561 | # link or a large artifact, and still inside the attention of the person who | ||
| 562 | # started the ship. | ||
| 563 | CONVERGE_TIMEOUT_S=300 | ||
| 564 | CONVERGE_POLL_S=5 | ||
| 565 | |||
| 566 | API="https://$CONSOLE_HOST" | ||
| 567 | |||
| 568 | # The credential is the one the plane already names for the smoke, chosen the | ||
| 569 | # same way the smoke chooses it for the VM lifecycle: the operator PAT from | ||
| 570 | # CI_PAT_FILE where there is one, and otherwise a console session signed in | ||
| 571 | # as CI_USER. The order matters — a plane may name both, and the PAT is then | ||
| 572 | # the identity whose tenant owns the fleet's hosts, which is exactly the | ||
| 573 | # identity that must offer them an upgrade and the one the smoke will place a | ||
| 574 | # VM as. A stage with a credential of its own would be a third token to | ||
| 575 | # issue, rotate and lose. | ||
| 576 | if [[ -z "${CI_USER:-}" && -z "${CI_PAT_FILE:-}" ]]; then | ||
| 577 | fail "$SHIP_ENV names no credential, so the fleet cannot be converged. | ||
| 578 | Set CI_USER + CI_PASSWORD_FILE for a plane with a password issuer, or | ||
| 579 | CI_PAT_FILE for one fronted by an external identity provider." | ||
| 580 | fi | ||
| 581 | |||
| 582 | # Everything the credential touches is a file in one 0700 directory that goes | ||
| 583 | # away with the run: the bearer header and the cookie jar are files rather | ||
| 584 | # than arguments, because an argument is visible in ps to every user on this | ||
| 585 | # machine. | ||
| 586 | cdir="$(mktemp -d)" | ||
| 587 | chmod 700 "$cdir" | ||
| 588 | trap 'rm -rf "$cdir"' EXIT | ||
| 589 | |||
| 590 | curl_auth=() | ||
| 591 | if [[ -n "${CI_PAT_FILE:-}" ]]; then | ||
| 592 | [[ -r "$CI_PAT_FILE" ]] || fail "CI_PAT_FILE names $CI_PAT_FILE, which cannot be read" | ||
| 593 | (umask 077; printf 'Authorization: Bearer %s\n' "$(cat "$CI_PAT_FILE")" >"$cdir/auth") | ||
| 594 | curl_auth=(-H "@$cdir/auth") | ||
| 595 | echo "acting as the operator whose PAT CI_PAT_FILE names" | ||
| 596 | else | ||
| 597 | : "${CI_PASSWORD_FILE:?set it alongside CI_USER in $SHIP_ENV}" | ||
| 598 | [[ -r "$CI_PASSWORD_FILE" ]] || fail "CI_PASSWORD_FILE names $CI_PASSWORD_FILE, which cannot be read" | ||
| 599 | # The same headless sign-in the smoke's credential chain walks, for the | ||
| 600 | # same reason it works there: GET /auth/login lands on the issuer's login | ||
| 601 | # form after the redirects, and the form takes the credentials posted back | ||
| 602 | # to the URL we landed on. Success is the session cookie in the jar and | ||
| 603 | # nothing else — a wrong password re-renders the form as a plain 200. | ||
| 604 | (umask 077; printf '%s' "$(cat "$CI_PASSWORD_FILE")" >"$cdir/pw") | ||
| 605 | form_url="$(curl -sS -L --max-time 30 -c "$cdir/jar" -b "$cdir/jar" \ | ||
| 606 | -o /dev/null -w '%{url_effective}' "$API/auth/login")" || | ||
| 607 | fail "GET $API/auth/login did not answer, so nothing can sign in to the plane just rolled" | ||
| 608 | curl -sS -L --max-time 30 -c "$cdir/jar" -b "$cdir/jar" -o /dev/null \ | ||
| 609 | --data-urlencode "email=$CI_USER" --data-urlencode "password@$cdir/pw" "$form_url" || | ||
| 610 | fail "posting $CI_USER's credentials to the issuer's login form at $form_url failed" | ||
| 611 | grep -q eitri_session "$cdir/jar" 2>/dev/null || | ||
| 612 | fail "signing in as $CI_USER left no session — check the identity exists in this | ||
| 613 | plane's issuer and that CI_PASSWORD_FILE holds its current password." | ||
| 614 | # The session in the jar IS the credential from here: the API takes the | ||
| 615 | # console cookie exactly where it takes a bearer token, so this stage needs | ||
| 616 | # no PAT of its own and leaves no token behind it. | ||
| 617 | curl_auth=(-b "$cdir/jar") | ||
| 618 | echo "signed in as $CI_USER" | ||
| 619 | fi | ||
| 620 | |||
| 621 | # One round trip: the body lands in api_body and the status in api_status, | ||
| 622 | # 000 when there was no answer at all (curl's own convention, and stage 3's). | ||
| 623 | api_body="" | ||
| 624 | api_status=0 | ||
| 625 | api_call() { | ||
| 626 | api_status="$(curl -sS --max-time 30 -X "$1" "${curl_auth[@]}" \ | ||
| 627 | -o "$cdir/resp" -w '%{http_code}' "$API$2")" || api_status=000 | ||
| 628 | api_body="$(cat "$cdir/resp" 2>/dev/null || true)" | ||
| 629 | } | ||
| 630 | |||
| 631 | # A host has converged when it reports THIS tag, compared as a string: the | ||
| 632 | # agent's version is stamped from the tag it was built at (scripts/release.sh) | ||
| 633 | # and stage 1 has already refused to ship a HEAD that is not exactly $TAG, so | ||
| 634 | # the two are the same characters or the host is not running this release. | ||
| 635 | # Every ordering question — is this host behind, may it be offered anything — | ||
| 636 | # stays with the plane, which answers it with internal/server/release and | ||
| 637 | # tells us in a status code. | ||
| 638 | started=$SECONDS | ||
| 639 | last_note=$SECONDS | ||
| 640 | offered=" " # hosts this stage has offered the upgrade to | ||
| 641 | announced=" " # hosts already reported as converged | ||
| 642 | skipped=" " # disconnected hosts, warned about once | ||
| 643 | refusal="" # the last thing the plane refused, for the timeout message | ||
| 644 | converged=0 | ||
| 645 | while :; do | ||
| 646 | api_call GET /api/v1/hosts | ||
| 647 | [[ "$api_status" == "200" ]] || | ||
| 648 | fail "GET /api/v1/hosts answered HTTP $api_status: ${api_body:-<empty>} | ||
| 649 | The plane is serving $TAG and the fleet has not been converged." | ||
| 650 | fleet="$(printf '%s' "$api_body" | jq -r '.[] | [ | ||
| 651 | .id, .name, (.online|tostring), (.agent_version // "-"), | ||
| 652 | (.pending_upgrade.version // "-"), (.pending_upgrade.age_s // 0 | tostring) | ||
| 653 | ] | @tsv')" || | ||
| 654 | fail "GET /api/v1/hosts did not answer with a list of hosts: ${api_body:-<empty>}" | ||
| 655 | |||
| 656 | # A plane with no hosts is a plane nobody has joined yet — a fresh one, or | ||
| 657 | # one whose fleet is still being built. There is nothing to converge and | ||
| 658 | # nothing wrong. The tenant is worth saying out loud: hosts are listed for | ||
| 659 | # the credential that asked, so a plane with a fleet and a credential from | ||
| 660 | # some other tenant looks exactly like this. | ||
| 661 | if [[ -z "$fleet" ]]; then | ||
| 662 | echo "no hosts are enrolled here for this credential's tenant — nothing to converge" | ||
| 663 | converged=0 | ||
| 664 | break | ||
| 665 | fi | ||
| 666 | |||
| 667 | converged=0 | ||
| 668 | waiting="" | ||
| 669 | while IFS=$'\t' read -r id name online version pending_v pending_age; do | ||
| 670 | [[ -n "$id" ]] || continue | ||
| 671 | |||
| 672 | if [[ "$version" == "$TAG" ]]; then | ||
| 673 | converged=$((converged + 1)) | ||
| 674 | case "$announced" in | ||
| 675 | *" $id "*) ;; | ||
| 676 | *) | ||
| 677 | case "$offered" in | ||
| 678 | *" $id "*) echo " $name: converged, now reporting $TAG" ;; | ||
| 679 | *) echo " $name: already at $TAG" ;; | ||
| 680 | esac | ||
| 681 | announced="$announced$id " ;; | ||
| 682 | esac | ||
| 683 | continue | ||
| 684 | fi | ||
| 685 | |||
| 686 | # A dark host blocks nothing. It cannot be handed an offer — the plane | ||
| 687 | # refuses one outright — and the smoke cannot place a VM on it either, | ||
| 688 | # so waiting for it would hold a release for a machine that has no part | ||
| 689 | # in proving it. It comes back to a fleet a release ahead of it and the | ||
| 690 | # console offers it the upgrade then. | ||
| 691 | if [[ "$online" != "true" ]]; then | ||
| 692 | case "$skipped" in | ||
| 693 | *" $id "*) ;; | ||
| 694 | *) | ||
| 695 | reported="$version" | ||
| 696 | if [[ "$reported" == "-" ]]; then reported="no version"; fi | ||
| 697 | warn "host $name is disconnected (last reported $reported) — skipped. | ||
| 698 | It cannot take an offer while it is dark, and the smoke cannot place a VM | ||
| 699 | on it either. Offer it the upgrade from the console when it returns." | ||
| 700 | skipped="$skipped$id " ;; | ||
| 701 | esac | ||
| 702 | continue | ||
| 703 | fi | ||
| 704 | |||
| 705 | # Connected and saying nothing about its version: an agent too old to | ||
| 706 | # name itself. The plane will not offer it anything (it has no version | ||
| 707 | # to order), and it would refuse to create a VM there as well, because | ||
| 708 | # that guest would get no certified host key. Neither waiting nor the | ||
| 709 | # smoke can improve on that, so say it now. | ||
| 710 | if [[ "$version" == "-" ]]; then | ||
| 711 | fail "host $name ($id) is connected but reports no agent version, so the plane | ||
| 712 | cannot offer it $TAG — and it would refuse to create a VM there. Upgrade | ||
| 713 | that agent by hand (docs/upgrade.md, 'Agents, by hand'), then resume: | ||
| 714 | scripts/ship.sh --target $TARGET --tag $TAG --from 8" | ||
| 715 | fi | ||
| 716 | |||
| 717 | # An offer for some other version is the plane naming a release that is | ||
| 718 | # not the one being shipped, and no amount of waiting turns one into the | ||
| 719 | # other. The usual cause is the manifest: the server reads it at boot | ||
| 720 | # and pins it for 24 hours, which is why stage 6 rolls the site first. | ||
| 721 | if [[ "$pending_v" != "-" && "$pending_v" != "$TAG" ]]; then | ||
| 722 | fail "host $name ($id) has been offered $pending_v, not $TAG: this plane's release | ||
| 723 | manifest does not name the tag being shipped. Check that the site rolled | ||
| 724 | (stage 6) and that the server restarted after it (stage 7)." | ||
| 725 | fi | ||
| 726 | |||
| 727 | if [[ "$pending_v" == "$TAG" ]]; then | ||
| 728 | waiting="$waiting $name (offered ${pending_age}s ago)" | ||
| 729 | continue | ||
| 730 | fi | ||
| 731 | |||
| 732 | # No offer standing: make one. This is also the re-offer path — a server | ||
| 733 | # restart forgets every outstanding offer, so a host we offered earlier | ||
| 734 | # can show up here again, and offering twice is exactly what a human | ||
| 735 | # would do. | ||
| 736 | api_call POST "/api/v1/hosts/$id/upgrade-agent" | ||
| 737 | case "$api_status" in | ||
| 738 | 202) | ||
| 739 | echo " $name: $version → $TAG (offered)" | ||
| 740 | offered="$offered$id " | ||
| 741 | waiting="$waiting $name (offered just now)" | ||
| 742 | ;; | ||
| 743 | 503) | ||
| 744 | # The plane has not fetched its release manifest yet, which is | ||
| 745 | # ordinary in the first seconds after stage 7 rolled the server. | ||
| 746 | # Wait for it rather than failing a release on a cold start. | ||
| 747 | refusal="$name: $api_body" | ||
| 748 | waiting="$waiting $name (plane has no release manifest yet)" | ||
| 749 | ;; | ||
| 750 | 409) | ||
| 751 | # internal/server/api/upgrade.go refuses three ways. "host is | ||
| 752 | # offline" is a race against the listing above and resolves itself | ||
| 753 | # — the next round sees the host dark and skips it. The other two | ||
| 754 | # (not behind the release, no artifact for this os/arch) need a | ||
| 755 | # person, and waiting out the timeout would only delay telling them. | ||
| 756 | if [[ "$api_body" == *"host is offline"* ]]; then | ||
| 757 | refusal="$name: $api_body" | ||
| 758 | waiting="$waiting $name (went offline mid-offer)" | ||
| 759 | else | ||
| 760 | fail "the plane refuses to offer $TAG to host $name ($id): $api_body" | ||
| 761 | fi | ||
| 762 | ;; | ||
| 763 | *) | ||
| 764 | fail "POST /api/v1/hosts/$id/upgrade-agent answered HTTP $api_status: ${api_body:-<empty>}" | ||
| 765 | ;; | ||
| 766 | esac | ||
| 767 | done <<<"$fleet" | ||
| 768 | |||
| 769 | [[ -n "$waiting" ]] || break | ||
| 770 | |||
| 771 | if [[ $((SECONDS - started)) -ge $CONVERGE_TIMEOUT_S ]]; then | ||
| 772 | fail "the fleet did not converge on $TAG within ${CONVERGE_TIMEOUT_S}s — | ||
| 773 | still waiting on:$waiting${refusal:+ | ||
| 774 | last refusal — $refusal} | ||
| 775 | These hosts are connected and not running $TAG. Where the offer was taken | ||
| 776 | and did not land, the reason is on the host, in the agent's log | ||
| 777 | (journalctl -u eitri-agent, or ~/Library/Logs/eitri-agent.log on a Mac); | ||
| 778 | where the plane refused it, the refusal above says why. The plane is | ||
| 779 | serving $TAG and is not proven. Resume with --from 8." | ||
| 780 | fi | ||
| 781 | |||
| 782 | if [[ $((SECONDS - last_note)) -ge 30 ]]; then | ||
| 783 | echo " waiting on:$waiting" | ||
| 784 | last_note=$SECONDS | ||
| 785 | fi | ||
| 786 | sleep "$CONVERGE_POLL_S" | ||
| 787 | done | ||
| 788 | |||
| 789 | skipped_n="$(printf '%s' "$skipped" | wc -w | tr -d ' ')" | ||
| 790 | summary="fleet: $converged host(s) reporting $TAG" | ||
| 791 | if [[ "$skipped_n" != "0" ]]; then | ||
| 792 | summary="$summary, $skipped_n disconnected and skipped" | ||
| 793 | fi | ||
| 794 | echo "$summary" | ||
| 795 | if [[ "$converged" == "0" && "$skipped_n" != "0" ]]; then | ||
| 796 | warn "no connected host is running $TAG, so the smoke has nowhere to place a VM." | ||
| 797 | fi | ||
| 798 | fi | ||
| 799 | |||
| 800 | # ── 9. Hosted smoke ─────────────────────────────────────────────────────────── | ||
| 529 | # The same binary the branch gate runs, pointed at this plane's public names. | 801 | # The same binary the branch gate runs, pointed at this plane's public names. |
| 530 | # It needs nothing but those names and a credential: every leg goes through the | 802 | # It needs nothing but those names and a credential: every leg goes through the |
| 531 | # plane's own front door, so a hosted run logs into no host in the fleet. What | 803 | # plane's own front door, so a hosted run logs into no host in the fleet. What |
| @@ -538,8 +810,8 @@ fi | |||
| 538 | # PAT; a plane fronted by a real identity provider sets an operator PAT instead, | 810 | # PAT; a plane fronted by a real identity provider sets an operator PAT instead, |
| 539 | # because there is no password to post at Google. No COVER_OUT either way — the | 811 | # because there is no password to post at Google. No COVER_OUT either way — the |
| 540 | # hosted binaries are not coverage-instrumented. | 812 | # hosted binaries are not coverage-instrumented. |
| 541 | if [[ "$FROM" -le 8 && "$SKIP_SMOKE" != "1" ]]; then | 813 | if [[ "$FROM" -le 9 && "$SKIP_SMOKE" != "1" ]]; then |
| 542 | bold "8. Hosted smoke against $TARGET" | 814 | bold "9. Hosted smoke against $TARGET" |
| 543 | if [[ -z "${CI_USER:-}" && -z "${CI_PAT_FILE:-}" ]]; then | 815 | if [[ -z "${CI_USER:-}" && -z "${CI_PAT_FILE:-}" ]]; then |
| 544 | fail "$SHIP_ENV names no credential for the smoke. | 816 | fail "$SHIP_ENV names no credential for the smoke. |
| 545 | Set CI_USER + CI_PASSWORD_FILE for a plane with a password issuer, or | 817 | Set CI_USER + CI_PASSWORD_FILE for a plane with a password issuer, or |
| @@ -574,11 +846,11 @@ if [[ "$FROM" -le 8 && "$SKIP_SMOKE" != "1" ]]; then | |||
| 574 | "$REPO_ROOT/bin/eitri-smoke" || | 846 | "$REPO_ROOT/bin/eitri-smoke" || |
| 575 | fail "hosted smoke FAILED against $TARGET — the plane is serving $TAG and is not proven." | 847 | fail "hosted smoke FAILED against $TARGET — the plane is serving $TAG and is not proven." |
| 576 | elif [[ "$SKIP_SMOKE" == "1" ]]; then | 848 | elif [[ "$SKIP_SMOKE" == "1" ]]; then |
| 577 | bold "8. Hosted smoke SKIPPED (--skip-smoke)" | 849 | bold "9. Hosted smoke SKIPPED (--skip-smoke)" |
| 578 | fi | 850 | fi |
| 579 | 851 | ||
| 580 | # ── 9. Report ───────────────────────────────────────────────────────────────── | 852 | # ── 10. Report ──────────────────────────────────────────────────────────────── |
| 581 | bold "9. $TARGET is serving $TAG" | 853 | bold "10. $TARGET is serving $TAG" |
| 582 | report_deployments=(eitri-server web) | 854 | report_deployments=(eitri-server web) |
| 583 | if [[ "$LOCAL_OIDC" == "1" ]]; then | 855 | if [[ "$LOCAL_OIDC" == "1" ]]; then |
| 584 | report_deployments+=(eitri-oidc) | 856 | report_deployments+=(eitri-oidc) |