diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be3140e..6b1cee1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,7 @@ jobs: - run: sh deploy/bootstrap_test.sh - run: sh deploy/uninstall_test.sh + - run: bash deploy/e2e_release_test.sh panel: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 060fa28..de39277 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,8 +6,12 @@ # install a full install from those assets, the way a release installs, then the same # assets again (a rerun must converge without restarting what did not change, # importing or uploading an image again, or touching Docker) -# upgrade the newest published release, then this commit's assets on top of it; -# skipped until a release exists +# readme the README's one-line install as a new host runs it today: this commit's +# installer on its default channel, which installs the newest published +# release's binary, images and plugin from that release's assets; skipped until +# a release exists +# upgrade the newest published release through its own installer and its own assets, +# then this commit's assets on top of it; skipped until a release exists # source a full install built on the host from this checkout (FELIS_SKIP_FETCH): # the fallback a release without assets, or an unsupported one, takes. It builds # everything with Docker, so it runs by hand and weekly only @@ -142,6 +146,60 @@ jobs: path: '*.log' if-no-files-found: ignore + # The README's command on a fresh host: this commit's installer, as main serves it, on its + # default channel with nothing pinned. It resolves the newest release and installs that + # release's binary, images and plugin from its assets, each checked against its + # SHA256SUMS; the private repo's token is the only thing added. FELIS_INSTALL_MODE picks the + # mode the setup console would ask for. + readme: + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Free disk space + run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + + - name: Find the newest release + id: release + env: + GH_TOKEN: ${{ github.token }} + run: bash deploy/e2e_release.sh find + + - name: Install as the README does + if: steps.release.outputs.tag != '' + env: + TOKEN: ${{ github.token }} + run: sudo FELIS_GITHUB_TOKEN="$TOKEN" FELIS_INSTALL_MODE=full bash deploy/bootstrap.sh 2>&1 | tee readme.log + + # The binary is the release's, so the phase is `release`: its database backup and + # timers are the release's to have or lack. + - name: Check the install + if: steps.release.outputs.tag != '' + env: + TAG: ${{ steps.release.outputs.tag }} + BINARY: ${{ steps.release.outputs.binary }} + SUMS: ${{ steps.release.outputs.sums }} + run: | + sudo bash deploy/e2e_check.sh release + sudo /usr/local/bin/felis version | grep -qx "felis ${TAG}" + bash deploy/e2e_release.sh check-readme readme.log + + - name: Diagnostics + if: failure() + run: | + export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + sudo -E /usr/local/bin/k3s kubectl get pods -A -o wide || true + sudo -E /usr/local/bin/k3s kubectl -n felis logs deploy/felis-postgres --tail=60 || true + sudo journalctl -u k3s -u felis-velocity -u docker --no-pager -n 120 || true + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + with: + name: e2e-readme-logs + path: '*.log' + if-no-files-found: ignore + upgrade: needs: artifacts runs-on: ubuntu-24.04 @@ -163,15 +221,13 @@ jobs: id: release env: GH_TOKEN: ${{ github.token }} - run: | - tag="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName 2>/dev/null || true)" - if [ -z "$tag" ]; then - echo "::notice::no published release yet; the upgrade path has nothing to start from" - fi - echo "tag=${tag}" >> "$GITHUB_OUTPUT" + run: bash deploy/e2e_release.sh find - # The release's own installer, fetching the release's own binary: what a host that - # installed that release is running today. + # The release's own installer on its default channel, fetching the release's own + # binary: what a host that installed that release is running today. FELIS_REF would + # build the tag from source instead, a path no host takes by default. FELIS_RELEASE pins + # the tag found above; an installer older than FELIS_RELEASE ignores it and resolves + # the newest release, the same tag. - name: Install the newest release if: steps.release.outputs.tag != '' env: @@ -179,11 +235,16 @@ jobs: TOKEN: ${{ github.token }} run: | git show "${TAG}:deploy/bootstrap.sh" > release-bootstrap.sh - sudo FELIS_GITHUB_TOKEN="$TOKEN" FELIS_REF="$TAG" FELIS_INSTALL_MODE=full bash release-bootstrap.sh 2>&1 | tee release.log + sudo FELIS_GITHUB_TOKEN="$TOKEN" FELIS_RELEASE="$TAG" FELIS_INSTALL_MODE=full bash release-bootstrap.sh 2>&1 | tee release.log - name: Check the release install if: steps.release.outputs.tag != '' - run: sudo bash deploy/e2e_check.sh release + env: + TAG: ${{ steps.release.outputs.tag }} + BINARY: ${{ steps.release.outputs.binary }} + run: | + sudo bash deploy/e2e_check.sh release + bash deploy/e2e_release.sh check-own release.log - name: Upgrade to this commit if: steps.release.outputs.tag != '' diff --git a/README.md b/README.md index cab9fe5..1454af3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A Kubernetes-driven Minecraft server hosting platform — one command to deploy, ## 使用方式 -在准备好的 Linux 主机上执行(已验证的发行版与架构见 [运维手册 §1](docs/operations.md#1-supported-hosts):CentOS Stream 9 aarch64 实机验证,Ubuntu 24.04 x86_64 每次推送由 CI 跑全新安装、重跑与升级): +在准备好的 Linux 主机上执行(已验证的发行版与架构见 [运维手册 §1](docs/operations.md#1-supported-hosts):CentOS Stream 9 aarch64 实机验证,Ubuntu 24.04 x86_64 每次推送由 CI 跑全新安装、重跑、升级和下面这条命令本身): ```bash curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash diff --git a/deploy/e2e_release.sh b/deploy/e2e_release.sh new file mode 100644 index 0000000..fd8d1c5 --- /dev/null +++ b/deploy/e2e_release.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# The newest published release, for the e2e workflow's readme and upgrade jobs, and what +# their installer logs must show about how that release reached the host: +# +# bash deploy/e2e_release.sh find # tag, binary, sums into $GITHUB_OUTPUT +# bash deploy/e2e_release.sh check-own LOG # the release's own installer (upgrade) +# bash deploy/e2e_release.sh check-readme LOG # this commit's installer on its default +# # channel, the README's command (readme) +# +# The checks read TAG, BINARY and SUMS from the environment, as find wrote them. The +# runners are x86_64, so the binary asset is felis-linux-amd64. deploy/e2e_release_test.sh +# holds this script's own checks, against bootstrap.sh's own messages. +set -euo pipefail + +ASSET=felis-linux-amd64 +HOST_BIN=/usr/local/bin/felis +fails=0 + +pass() { printf 'PASS %s\n' "$*"; } +fail() { printf 'FAIL %s\n' "$*"; fails=$((fails + 1)); } +has() { # label fixed-string log + if grep -qF -- "$2" "$3"; then pass "$1"; else fail "$1: no line with <$2> in $3"; fi +} +has_re() { # label regex log + if grep -qE -- "$2" "$3"; then pass "$1"; else fail "$1: no line matching <$2> in $3"; fi +} +lacks_re() { # label regex log + local hit + if hit="$(grep -E -m 1 -- "$2" "$3")"; then fail "$1: ${hit}"; else pass "$1"; fi +} + +# find_release: `gh release view` with no tag answers with the newest release that is not a +# prerelease, the one the installer's release channel resolves. +find_release() { + local lines tag names binary="" sums="" + lines="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName,assets --jq '.tagName, .assets[].name' 2>/dev/null || true)" + tag="$(printf '%s\n' "$lines" | head -n 1)" + names="$(printf '%s\n' "$lines" | tail -n +2)" + if [ -z "$tag" ]; then + echo "::notice::no published release yet; the readme and upgrade jobs have nothing to install" + fi + if printf '%s\n' "$names" | grep -qxF "$ASSET"; then binary=yes; fi + if printf '%s\n' "$names" | grep -qxF SHA256SUMS; then sums=yes; fi + printf 'tag=%s\nbinary=%s\nsums=%s\n' "$tag" "$binary" "$sums" >> "${GITHUB_OUTPUT:-/dev/stdout}" +} + +# check_own: a release's installer, however old, downloads the release's binary when the +# release publishes one, and says so in the same words. +check_own() { + local log="$1" + if [ "${BINARY:-}" != yes ]; then + echo "::notice::release ${TAG} publishes no ${ASSET}; its installer builds it from source" + return 0 + fi + has "the release's installer installed the release's binary" "installed ${ASSET} ${TAG} at ${HOST_BIN}" "$log" +} + +# check_readme: this commit's installer takes everything from a release that publishes its +# SHA256SUMS and builds nothing on the host; from one without, it builds the tag from source +# and says why. +check_readme() { + local log="$1" role + if [ "${BINARY:-}" = yes ] && [ "${SUMS:-}" = yes ]; then + has "the binary is the release's" "installed ${ASSET} ${TAG} at ${HOST_BIN}" "$log" + has "the images and plugin come from the release" "release ${TAG}'s prebuilt images and Velocity plugin are installed as published" "$log" + for role in felis limbo lobby paper; do + has_re "felis/${role} is the release's" "felis/${role}:[^ ]* is the release's" "$log" + done + has_re "the registry image is the release's" "docker.io/library/registry@sha256:[0-9a-f]* is the release's" "$log" + has_re "the postgres image is the release's" "docker.io/library/postgres@sha256:[0-9a-f]* is the release's" "$log" + has "felis-velocity.jar is the release's" "felis-velocity.jar is the release's" "$log" + lacks_re "nothing fell back to a build or a pull" "on this host instead|from Docker Hub instead|building felis from source" "$log" + lacks_re "Docker was left alone" "docker already installed|installing docker|docker running" "$log" + elif [ "${BINARY:-}" = yes ]; then + has "the source build says why" "release ${TAG} publishes no SHA256SUMS, so ${ASSET} cannot be verified; building ${TAG} from source on this host instead" "$log" + echo "::warning::release ${TAG} publishes no SHA256SUMS, so the README's install builds ${TAG} from source on the host, Docker included; a release cut by release.yml puts it on the assets" + else + has "the source build says why" "release ${TAG} publishes no usable ${ASSET}; building ${TAG} from source on this host instead" "$log" + echo "::warning::release ${TAG} publishes no ${ASSET}, so the README's install builds ${TAG} from source on the host, Docker included; a release cut by release.yml puts it on the assets" + fi +} + +case "${1:-}" in + find) find_release ;; + check-own) check_own "${2:?usage: e2e_release.sh check-own LOG}" ;; + check-readme) check_readme "${2:?usage: e2e_release.sh check-readme LOG}" ;; + *) + echo "usage: e2e_release.sh find | check-own LOG | check-readme LOG" >&2 + exit 2 + ;; +esac +exit "$fails" diff --git a/deploy/e2e_release_test.sh b/deploy/e2e_release_test.sh new file mode 100644 index 0000000..2bae9a3 --- /dev/null +++ b/deploy/e2e_release_test.sh @@ -0,0 +1,168 @@ +#!/bin/bash +# Checks for deploy/e2e_release.sh. Run it as: bash deploy/e2e_release_test.sh +# +# The installer logs it reads are built from bootstrap.sh's own ok/warn messages, expanded +# with a release's values, so rewording one of them there fails here rather than in a +# two-hour e2e run. gh is a stub that prints what a release listing would. +set -u + +here="$(dirname "$0")" +ER="${1:-${here}/e2e_release.sh}" +BS="${2:-${here}/bootstrap.sh}" +[ -f "$ER" ] || { echo "no such script: $ER"; exit 1; } +[ -f "$BS" ] || { echo "no such script: $BS"; exit 1; } +fails=0 + +expect() { # label needle haystack + case "$3" in + *"$2"*) echo "PASS $1" ;; + *) echo "FAIL $1: expected <$2> in:"; echo "$3"; fails=$((fails + 1)) ;; + esac +} +status() { # label want got + if [ "$2" = "$3" ]; then echo "PASS $1"; else echo "FAIL $1: exit $3, want $2"; fails=$((fails + 1)); fi +} + +root="$(mktemp -d)" +trap 'rm -rf "$root"' EXIT + +# msg prints bootstrap.sh's first ok/warn message holding , expanded with +# the variables below the way the installer expands it. They are read only through that eval. +# shellcheck disable=SC2034 +{ + tag=v1.2.3 + FELIS_REF="$tag" + v="$tag" + asset=felis-linux-amd64 + name="$asset" + HOST_BIN=/usr/local/bin/felis + digest=sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef +} +msg() { + local line body + line="$(grep -F -- "$1" "$BS" | grep -E '^[[:space:]]*(ok|warn) "' | head -n 1)" + if [ -z "$line" ]; then + echo "FAIL bootstrap.sh prints no message holding <$1>" >&2 + fails=$((fails + 1)) + return + fi + body="${line#*\"}" + body="${body%\"*}" + eval "printf '%s\n' \"${body}\"" +} +# unusable prints the warning of bootstrap.sh's first artifact_unusable call holding +# ; artifact_unusable joins its two arguments with "; ". +unusable() { + local line + line="$(grep -F -- "$1" "$BS" | grep -E '^[[:space:]]*artifact_unusable "' | head -n 1)" + if [ -z "$line" ]; then + echo "FAIL bootstrap.sh has no artifact_unusable call holding <$1>" >&2 + fails=$((fails + 1)) + return + fi + artifact_unusable() { printf '%s; %s\n' "$1" "$2"; } + eval "$line" +} +image_line() { # target + # shellcheck disable=SC2034 # read by msg's eval + local target="$1" + msg 'is the release'"'"'s (${digest:7:12})' +} + +# What an install from a complete release prints, in the installer's order. +{ + msg 'ok "installed ${asset} ${FELIS_REF} at ${HOST_BIN}"' + msg 'matches release ${tag}'"'"'s SHA256SUMS' + msg 'prebuilt images and Velocity plugin are installed as published' + for t in felis/felis:v1.2.3 felis/limbo:v1.2.3 felis/lobby:v1.2.3 felis/paper:v1.2.3 \ + docker.io/library/registry@sha256:aa docker.io/library/postgres@sha256:bb; do + image_line "$t" + done + msg 'felis-velocity.jar is the release'"'"'s"' +} > "$root/complete.log" + +readme() { # log binary sums + TAG="$tag" BINARY="$2" SUMS="$3" bash "$ER" check-readme "$1" 2>&1 +} +own() { # log binary + TAG="$tag" BINARY="$2" bash "$ER" check-own "$1" 2>&1 +} + +out="$(readme "$root/complete.log" yes yes)" +status "a complete release's install passes" 0 $? +expect " and every image is checked" "PASS felis/paper is the release's" "$out" + +grep -v 'felis/limbo:' "$root/complete.log" > "$root/nolimbo.log" +out="$(readme "$root/nolimbo.log" yes yes)" +status "an image missing from the log fails" 1 $? +expect " and names it" "FAIL felis/limbo is the release's" "$out" + +{ + cat "$root/complete.log" + name=felis-images-linux-amd64.txt unusable '} cannot be used" "building its images on this host instead"' +} > "$root/fallback.log" +out="$(readme "$root/fallback.log" yes yes)" +status "an image built on the host fails" 1 $? +expect " and quotes the fallback" "building its images on this host instead" "$out" + +{ cat "$root/complete.log"; msg 'ok "docker already installed"'; } > "$root/docker.log" +out="$(readme "$root/docker.log" yes yes)" +status "Docker touched fails" 1 $? + +sed 's/installed felis-linux-amd64 v1.2.3/installed felis-linux-amd64 v1.2.2/' "$root/complete.log" > "$root/other.log" +out="$(readme "$root/other.log" yes yes)" +status "another release's binary fails" 1 $? + +# A release that publishes its binary but no SHA256SUMS: this commit's installer builds the +# tag from source and says so. +msg 'publishes no SHA256SUMS, so ${name} cannot be verified' > "$root/nosums.log" +out="$(readme "$root/nosums.log" yes "")" +status "a release without SHA256SUMS passes when the fallback is announced" 0 $? +expect " and warns in the run" "::warning::release v1.2.3 publishes no SHA256SUMS" "$out" +out="$(readme "$root/complete.log" yes "")" +status "a release without SHA256SUMS fails when nothing announced the fallback" 1 $? + +msg 'publishes no usable ${asset}' > "$root/nobinary.log" +out="$(readme "$root/nobinary.log" "" "")" +status "a release without a binary passes when the fallback is announced" 0 $? +out="$(readme "$root/nosums.log" "" "")" +status "a release without a binary fails when the log says otherwise" 1 $? + +# check-own: the release's own installer, which may predate SHA256SUMS. +out="$(own "$root/complete.log" yes)" +status "the release's installer downloading its binary passes" 0 $? +out="$(own "$root/nobinary.log" yes)" +status "the release's installer building from source fails" 1 $? +out="$(own "$root/other.log" yes)" +status "the release's installer downloading another release fails" 1 $? +out="$(own "$root/nobinary.log" "")" +status "a release without a binary asks nothing of its installer" 0 $? + +# find: the listing gh answers with, in the step's outputs, exactly. +mkdir -p "$root/bin" +cat > "$root/bin/gh" <<'STUB' +#!/bin/sh +[ -n "${GH_LISTING:-}" ] || exit 1 +printf '%s\n' $GH_LISTING +STUB +chmod +x "$root/bin/gh" +find_run() { # listing: prints what find says; its outputs land in $root/out + : > "$root/out" + GH_LISTING="$1" GITHUB_REPOSITORY=FelisMC/Felis GITHUB_OUTPUT="$root/out" PATH="$root/bin:$PATH" bash "$ER" find 2>&1 +} +same() { # label want got + if [ "$2" = "$3" ]; then echo "PASS $1"; else printf 'FAIL %s: got\n%s\nwant\n%s\n' "$1" "$3" "$2"; fails=$((fails + 1)); fi +} +find_run "v1.2.3 felis-linux-amd64 felis-linux-arm64 SHA256SUMS felis-velocity.jar" >/dev/null +same "find: a complete release" "$(printf 'tag=v1.2.3\nbinary=yes\nsums=yes')" "$(cat "$root/out")" +find_run "v0.1.0 felis-linux-amd64 felis-linux-arm64" >/dev/null +same "find: a release without SHA256SUMS" "$(printf 'tag=v0.1.0\nbinary=yes\nsums=')" "$(cat "$root/out")" +find_run "v0.1.0 felis-linux-arm64 felis-linux-amd64.cdx.json SHA256SUMS.sig" >/dev/null +same "find: only exact asset names count" "$(printf 'tag=v0.1.0\nbinary=\nsums=')" "$(cat "$root/out")" +out="$(find_run "")" +same "find: no release" "$(printf 'tag=\nbinary=\nsums=')" "$(cat "$root/out")" +expect " and says so" "::notice::no published release yet" "$out" + +echo +if [ "$fails" -eq 0 ]; then echo "ALL PASS"; else echo "${fails} FAILED"; fi +exit "$fails" diff --git a/docs/operations.md b/docs/operations.md index 1bc67aa..f390713 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -21,7 +21,7 @@ release pins by digest, with its data on the host in `/var/lib/felis/postgres`. | OS family | Package manager | Architectures | Status | |---|---|---|---| | CentOS Stream 9 (firewalld active, SELinux enforcing) | dnf | aarch64 | **[VM-VERIFIED]** fresh install from release assets and its rerun, upgrade from v0.1.0 (moving the database off the host PostgreSQL 13 into felis-postgres), uninstall and reinstall | -| Ubuntu 24.04 LTS | apt | x86_64 | **[CI]** fresh install and same-commit rerun from the pushed commit's release assets, and upgrade from the newest release onto them; the on-host build weekly | +| Ubuntu 24.04 LTS | apt | x86_64 | **[CI]** fresh install and same-commit rerun from the pushed commit's release assets; the README's one-line install as a new host runs it (the newest release's own assets); upgrade from the newest release, installed from its assets by its own installer, onto them; the on-host build weekly | | RHEL / Rocky / Alma 9, Fedora | dnf | x86_64, aarch64 | [CODE-ONLY] same code path as CentOS Stream | | Debian 12, other Ubuntu releases | apt | x86_64, aarch64 | [CODE-ONLY] | | openSUSE Leap / Tumbleweed | zypper | x86_64, aarch64 | [CODE-ONLY] |