fix k8s v1.33.3 etcd image version (#21337) #100
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
# Functional Test is a subset of minikube integration test, testing the most essential features of minikube. | |
name: Functional Test | |
on: | |
workflow_dispatch: | |
pull_request: | |
paths: | |
- "go.mod" | |
- "**.go" | |
- "Makefile" | |
- "!site/**" | |
- "!**.md" | |
- "!**.json" | |
push: | |
branches: [ master ] | |
paths: | |
- "go.mod" | |
- "**.go" | |
- "Makefile" | |
- "!site/**" | |
- "!**.md" | |
- "!**.json" | |
# Limit one functional test job running per PR/Branch | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
# For example, if you push multiple commits to a pull request in quick succession, only the latest workflow run will continue | |
cancel-in-progress: true | |
env: | |
GOPROXY: https://proxy.golang.org | |
GO_VERSION: '1.24.0' | |
permissions: | |
contents: read | |
jobs: | |
# build-test-binaries job runs before all other jobs and produces binaries/test-data to be shared by all following jobs | |
build-test-binaries: | |
runs-on: ubuntu-22.04 | |
steps: | |
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 | |
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 | |
with: | |
go-version: ${{env.GO_VERSION}} | |
cache: true | |
- name: Download Dependencies | |
run: go mod download | |
- name: Build minikube and e2e test binaries | |
run: | | |
make e2e-linux-amd64 e2e-darwin-amd64 | |
cp -r test/integration/testdata ./out | |
- name: Upload Test Binaries | |
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
with: | |
name: binaries | |
path: out | |
functional-test: | |
name: ${{ matrix.name }} | |
needs: build-test-binaries | |
runs-on: ${{ matrix.os }} | |
permissions: | |
contents: none | |
strategy: | |
fail-fast: false | |
matrix: | |
include: | |
- name: docker-docker-ubuntu22.04-x86_64 | |
driver: docker | |
cruntime: docker | |
os: ubuntu-22.04 | |
test-timeout: 15m | |
- name: docker-containerd-ubuntu-22.04-x86_64 | |
driver: docker | |
cruntime: containerd | |
extra-start-args: --container-runtime=containerd | |
os: ubuntu-22.04 | |
test-timeout: 15m | |
- name: docker-containerd-rootless-ubuntu-22.04-x86_64 | |
driver: docker | |
cruntime: containerd | |
os: ubuntu-22.04 | |
extra-start-args: --container-runtime=containerd --rootless | |
rootless: true | |
test-timeout: 15m | |
- name: podman-docker-ubuntu-24.04-x86_64 | |
driver: podman | |
cruntime: docker | |
os: ubuntu-24.04 | |
test-timeout: 15m | |
- name: baremetal-docker-ubuntu-22.04-x86_64 | |
driver: none | |
cruntime: docker | |
os: ubuntu-22.04 | |
test-timeout: 7m | |
- name: qemu-docker-macos-13-x86_64 | |
driver: qemu | |
cruntime: docker | |
os: macos-13 | |
extra-start-args: --network socket_vmnet | |
test-timeout: 50m | |
- name: vfkit-docker-macos-13-x86_64 | |
driver: vfkit | |
cruntime: docker | |
os: macos-13 | |
extra-start-args: --network vmnet-shared | |
test-timeout: 50m | |
steps: | |
- name: Info Block (macOS) | |
if: runner.os == 'macOS' | |
shell: bash | |
run: | | |
set -x | |
echo "=== Kernel and OS ===" | |
uname -a | |
sw_vers | |
sysctl kern.osrelease kern.osversion kern.version || true | |
echo "=== CPU ===" | |
sysctl -n machdep.cpu.brand_string | |
sysctl -n hw.ncpu | |
sysctl -n machdep.cpu.core_count || true | |
sysctl -n machdep.cpu.thread_count || true | |
sysctl -n machdep.cpu.features || true | |
sysctl -n machdep.cpu.leaf7_features || true | |
sysctl -n machdep.cpu.extfeatures || true | |
echo "=== Memory ===" | |
sysctl -n hw.memsize | |
echo "$(sysctl -n hw.memsize) / 1024 / 1024 / 1024" | bc | |
sysctl -n hw.pagesize || true | |
vm_stat || true | |
echo "=== Virtualization ===" | |
sysctl -n kern.hv_vmm_present | |
sysctl -n kern.hv_support | |
sysctl -n machdep.cpu.features | grep -i vmx || true | |
echo "=== Hardware Inventory ===" | |
sysctl hw.model | |
system_profiler SPHardwareDataType | |
echo "=== Storage ===" | |
diskutil list || true | |
df -h | |
system_profiler SPStorageDataType | sed -n '1,200p' || true | |
echo "=== Network ===" | |
ifconfig | |
networksetup -listallhardwareports || true | |
scutil --get HostName || true | |
scutil --get LocalHostName || true | |
scutil --get ComputerName || true | |
route -n get default || true | |
netstat -rn || true | |
scutil --dns || true | |
echo "=== Uptime and Load ===" | |
uptime | |
sysctl kern.boottime || true | |
echo "=== Security ===" | |
spctl --status || true | |
csrutil status || true | |
- name: Info Block (linux) | |
if: runner.os == 'Linux' | |
shell: bash | |
run: | | |
set -x | |
echo "=== Kernel and OS ===" | |
uname -a | |
cat /etc/os-release | |
echo "=== CPU ===" | |
nproc | |
grep -m1 'model name' /proc/cpuinfo || true | |
grep -m1 '^flags' /proc/cpuinfo || true | |
lscpu || true | |
lscpu | grep -i 'hypervisor\|virt' || true | |
echo "=== Memory ===" | |
grep MemTotal /proc/meminfo || true | |
awk '/MemTotal/ {printf "%.2f\n", $2 / 1024 / 1024}' /proc/meminfo || true | |
free -h || true | |
echo "=== Virtualization ===" | |
systemd-detect-virt || true | |
# CPU virtualization flags present | |
egrep -c '(vmx|svm)' /proc/cpuinfo || true | |
# KVM modules and nested virtualization status | |
lsmod | grep -E '(^kvm|kvm_(intel|amd))' || true | |
if [ -f /sys/module/kvm_intel/parameters/nested ]; then | |
echo -n "kvm_intel nested: "; cat /sys/module/kvm_intel/parameters/nested | |
fi | |
if [ -f /sys/module/kvm_amd/parameters/nested ]; then | |
echo -n "kvm_amd nested: "; cat /sys/module/kvm_amd/parameters/nested | |
fi | |
sudo journalctl -k | grep -i kvm | tail -n 100 || true | |
if command -v virt-host-validate >/dev/null 2>&1; then | |
sudo virt-host-validate || true | |
fi | |
echo "=== Hardware Inventory ===" | |
sudo dmidecode -s system-manufacturer || true | |
sudo dmidecode -s system-product-name || true | |
sudo dmidecode -t bios -t system -t processor -t memory | sed -n '1,200p' || true | |
sudo lshw -short || true | |
lspci -nn || true | |
lsusb || true | |
echo "=== Storage ===" | |
lsblk -o NAME,MODEL,SIZE,TYPE,MOUNTPOINT,FSTYPE || true | |
df -h || true | |
echo "=== Network ===" | |
ip addr show || true | |
ip route || true | |
ip -s link || true | |
ifconfig || true | |
echo "=== Cgroups ===" | |
stat -fc %T /sys/fs/cgroup || true | |
echo "=== Uptime and Load ===" | |
uptime || true | |
who -b || true | |
cat /proc/loadavg || true | |
awk '{printf "Uptime (seconds): %s\n", $1}' /proc/uptime 2>/dev/null || true | |
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 | |
with: | |
go-version: ${{env.GO_VERSION}} | |
cache: true | |
- name: Install gopogh | |
shell: bash | |
run: | | |
GOPOGH_VERSION=v0.29.0 | |
GOOS=$(go env GOOS) | |
GOARCH=$(go env GOARCH) | |
URL="https://github.com/medyagh/gopogh/releases/download/${GOPOGH_VERSION}/gopogh-${GOOS}-${GOARCH}" | |
echo "Downloading ${URL}" | |
curl -fsSL "${URL}" -o gopogh | |
sudo install -m 0755 gopogh /usr/local/bin/gopogh | |
rm gopogh | |
command -v gopogh | |
gopogh -version || true | |
- name: Set up cgroup v2 delegation (rootless) | |
if: ${{ matrix.rootless }} | |
run: | | |
sudo mkdir -p /etc/systemd/system/[email protected] | |
cat <<EOF | sudo tee /etc/systemd/system/[email protected]/delegate.conf | |
[Service] | |
Delegate=cpu cpuset io memory pids | |
EOF | |
sudo systemctl daemon-reload | |
- name: Set up Rootless Docker (rootless) | |
if: ${{ matrix.rootless }} | |
run: | | |
sudo apt-get remove moby-engine-* | |
curl https://get.docker.com | sudo sh | |
dockerd-rootless-setuptool.sh install -f | |
docker context use rootless | |
- name: Ensure bootpd is enabled (macos-13) | |
if: matrix.os == 'macos-13' | |
shell: bash | |
run: | | |
set -x | |
fw=/usr/libexec/ApplicationFirewall/socketfilterfw | |
sudo $fw --remove /usr/libexec/bootpd | |
sudo $fw --add /usr/libexec/bootpd | |
sudo $fw --unblock /usr/libexec/bootpd | |
- name: Update brew package index (macos) | |
if: runner.os == 'macOS' | |
run: brew update | |
- name: Update apt-get package index (ubuntu) | |
if: runner.os == 'Linux' && (matrix.driver == 'podman' || matrix.driver == 'none') | |
run: sudo apt-get update -qq | |
- name: Install cri_dockerd (baremetal only) | |
shell: bash | |
if: matrix.driver == 'none' | |
run: | | |
CRI_DOCKERD_VERSION="v0.4.0" | |
CRI_DOCKERD_COMMIT="b9b889355f3002c01db294427964e454dfbc3feb" | |
CRI_DOCKERD_BASE_URL="https://storage.googleapis.com/kicbase-artifacts/cri-dockerd/${CRI_DOCKERD_COMMIT}" | |
sudo curl -L "${CRI_DOCKERD_BASE_URL}/amd64/cri-dockerd" -o /usr/bin/cri-dockerd | |
sudo curl -L "${CRI_DOCKERD_BASE_URL}/cri-docker.socket" -o /usr/lib/systemd/system/cri-docker.socket | |
sudo curl -L "${CRI_DOCKERD_BASE_URL}/cri-docker.service" -o /usr/lib/systemd/system/cri-docker.service | |
sudo chmod +x /usr/bin/cri-dockerd | |
- name: Install crictl (baremetal only) | |
shell: bash | |
if: matrix.driver == 'none' | |
run: | | |
CRICTL_VERSION="v1.28.0" | |
curl -L https://github.com/kubernetes-sigs/cri-tools/releases/download/$CRICTL_VERSION/crictl-${CRICTL_VERSION}-linux-amd64.tar.gz --output crictl-${CRICTL_VERSION}-linux-amd64.tar.gz | |
sudo tar zxvf crictl-$CRICTL_VERSION-linux-amd64.tar.gz -C /usr/local/bin | |
# conntrack is required for kubernetes 1.18 and higher | |
- name: Install conntrack & socat (baremetal only) | |
shell: bash | |
if: matrix.driver == 'none' | |
run: sudo apt-get -qq -y install conntrack | |
# socat is required for kubectl port forward which is used in some tests | |
- name: Install socat (baremetal only) | |
shell: bash | |
if: matrix.driver == 'none' | |
run: sudo apt-get -qq -y install socat | |
- name: Install container networking plugins (baremetal only) | |
shell: bash | |
if: matrix.driver == 'none' | |
run: | | |
CNI_PLUGIN_VERSION="v1.7.1" | |
CNI_PLUGIN_TAR="cni-plugins-linux-amd64-$CNI_PLUGIN_VERSION.tgz" | |
CNI_PLUGIN_INSTALL_DIR="/opt/cni/bin" | |
curl -LO "https://github.com/containernetworking/plugins/releases/download/$CNI_PLUGIN_VERSION/$CNI_PLUGIN_TAR" | |
sudo mkdir -p "$CNI_PLUGIN_INSTALL_DIR" | |
sudo tar -xf "$CNI_PLUGIN_TAR" -C "$CNI_PLUGIN_INSTALL_DIR" | |
rm "$CNI_PLUGIN_TAR" | |
- name: Install docker-cli | |
run: | | |
set -x | |
if [[ "$RUNNER_OS" == "macOS" ]]; then | |
brew install docker | |
fi | |
echo "=== Docker Version ===" | |
docker version || true | |
if [[ "$RUNNER_OS" == "Linux" ]]; then | |
echo "=== Docker Info ===" | |
docker info || true | |
echo "=== Docker Disk Usage ===" | |
docker system df || true | |
echo "=== Docker System Info (JSON) ===" | |
docker system info --format='{{json .}}' | { command -v jq >/dev/null 2>&1 && jq . || cat; } || true | |
echo "=== Running Containers ===" | |
docker ps -a || true | |
echo "=== Images ===" | |
docker images || true | |
fi | |
- name: Install podman | |
if: matrix.driver == 'podman' | |
shell: bash | |
run: | | |
sudo apt -q update | |
sudo apt install -q -y podman | |
lsb_release -a || true | |
echo "=== podman version ===" | |
podman version || true | |
echo "=== podman info ===" | |
podman info || true | |
echo "=== podman system df ===" | |
podman system df || true | |
echo "=== podman system info (JSON) ===" | |
podman system info --format='{{json .}}' || true | |
echo "=== podman ps ===" | |
podman ps || true | |
- name: Install kubectl | |
shell: bash | |
run: | | |
if [[ "$RUNNER_OS" == "macOS" ]]; then | |
brew install kubectl | |
elif [[ "$RUNNER_OS" == "Linux" ]]; then | |
tmp=$(mktemp) | |
curl -fsSL -o "$tmp" "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" | |
sudo install -m 0755 "$tmp" /usr/local/bin/kubectl | |
rm "$tmp" | |
else | |
echo "Unsupported OS: $RUNNER_OS" | |
exit 1 | |
fi | |
kubectl version --client=true | |
- name: Install qemu and socket_vmnet (macos) | |
if: matrix.os == 'macos-13' && matrix.driver == 'qemu' | |
run: | | |
brew install qemu socket_vmnet | |
HOMEBREW=$(which brew) && sudo ${HOMEBREW} services start socket_vmnet | |
- name: Install vfkit and vmnet_helper (macos) | |
if: matrix.os == 'macos-13' && matrix.driver == 'vfkit' | |
run: | | |
brew install vfkit | |
machine="$(uname -m)" | |
archive="vmnet-helper-$machine.tar.gz" | |
curl -LOf "https://github.com/nirs/vmnet-helper/releases/latest/download/$archive" | |
sudo tar xvf "$archive" -C / opt/vmnet-helper | |
sudo install -m 0640 /opt/vmnet-helper/share/doc/vmnet-helper/sudoers.d/vmnet-helper /etc/sudoers.d/ | |
- name: Download Test Binaries | |
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 | |
with: | |
name: binaries | |
- name: Disable AppArmor for MySQL | |
if: runner.os == 'Linux' | |
run: | | |
sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/ | |
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld | |
- name: Run Integration Test | |
id: run_test | |
continue-on-error: true | |
shell: bash | |
run: | | |
set -x | |
mkdir -p report | |
chmod a+x ./e2e-* | |
chmod a+x ./minikube-* | |
./minikube-$(go env GOOS)-$(go env GOARCH) delete --all --purge | |
START_TIME=$(date -u +%s) | |
./e2e-$(go env GOOS)-$(go env GOARCH) -minikube-start-args=" --driver=${{ matrix.driver }} ${{ matrix.extra-start-args }} -v=6 --alsologtostderr" -test.run TestFunctional -test.timeout=${{ matrix.test-timeout }} -test.v -binary=./minikube-$(go env GOOS)-$(go env GOARCH) 2>&1 | tee ./report/testout.txt | |
END_TIME=$(date -u +%s) | |
TIME_ELAPSED=$(($END_TIME-$START_TIME)) | |
min=$((${TIME_ELAPSED}/60)) | |
sec=$((${TIME_ELAPSED}%60)) | |
TIME_ELAPSED="${min} min $sec seconds " | |
# make variables available for next step | |
echo "TIME_ELAPSED=${TIME_ELAPSED}" >> $GITHUB_ENV | |
- name: Generate Gopogh HTML Report | |
if: always() | |
shell: bash | |
run: | | |
# Check if the test step failed AND the log contains "timed out" | |
if [[ "${{ steps.run_test.outcome }}" == "failure" && $(grep -c "timed out" ./report/testout.txt) -gt 0 ]]; then | |
# If it was a timeout, set your custom message | |
RESULT_SHORT="⌛⌛⌛ Test Timed out ${TIME_ELAPSED} ⌛⌛⌛" | |
else | |
go tool test2json -t < ./report/testout.txt > ./report/testout.json || true | |
STAT=$(gopogh -in ./report/testout.json -out_html ./report/testout.html -out_summary ./report/testout_summary.json -name "${{ matrix.name }} ${GITHUB_REF}" -repo "${GITHUB_REPOSITORY}" -details "${GITHUB_SHA}") || true | |
PassNum=$(echo $STAT | jq '.NumberOfPass') | |
FailNum=$(echo $STAT | jq '.NumberOfFail') | |
TestsNum=$(echo $STAT | jq '.NumberOfTests') | |
if [ "${FailNum}" -eq 0 ]; then | |
STATUS_ICON="✓" | |
else | |
STATUS_ICON="✗" | |
fi | |
if [ "${PassNum}" -eq 0 ]; then | |
STATUS_ICON="✗" | |
fi | |
# Result in one sentence | |
RESULT_SHORT="${STATUS_ICON} Completed with ${FailNum} / ${TestsNum} failures in ${TIME_ELAPSED}" | |
fi | |
echo "RESULT_SHORT=${RESULT_SHORT}" >> $GITHUB_ENV | |
echo "TIME_ELAPSED=${TIME_ELAPSED}" >> $GITHUB_ENV | |
echo 'STAT<<EOF' >> $GITHUB_ENV | |
echo "${STAT}" >> $GITHUB_ENV | |
echo 'EOF' >> $GITHUB_ENV | |
- name: Set PR or Branch label for report filename | |
id: vars | |
run: | | |
if [ "${{ github.event_name }}" = "pull_request" ]; then | |
echo "PR_OR_MASTER=PR${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT | |
else | |
echo "PR_OR_MASTER=Master" >> $GITHUB_OUTPUT | |
fi | |
echo "COMMIT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT | |
RUN_ID_SHORT="$GITHUB_RUN_ID" | |
if [ ${#RUN_ID_SHORT} -gt 7 ]; then | |
RUN_ID_SHORT="${RUN_ID_SHORT: -7}" | |
fi | |
echo "RUN_ID_SHORT=${RUN_ID_SHORT}" >> $GITHUB_OUTPUT | |
- name: Upload Gopogh report | |
id: upload_gopogh | |
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
with: | |
name: functional-${{ matrix.name }}-${{ steps.vars.outputs.PR_OR_MASTER }}-sha-${{ steps.vars.outputs.COMMIT_SHA }}-run-${{ steps.vars.outputs.RUN_ID_SHORT}} | |
path: ./report | |
- name: The End Result Summary ${{ matrix.name }} | |
shell: bash | |
run: | | |
summary="$GITHUB_STEP_SUMMARY" | |
Print_Gopogh_Artifact_Download_URL() { | |
ARTIFACT_NAME="functional-${{ matrix.name }}-${{ steps.vars.outputs.PR_OR_MASTER }}-sha-${{ steps.vars.outputs.COMMIT_SHA }}" | |
ARTIFACT_ID='${{ steps.upload_gopogh.outputs.artifact-id }}' | |
if [ -n "$ARTIFACT_ID" ]; then | |
URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" | |
echo "Gopogh report artifact ($ARTIFACT_NAME): $URL" | |
echo "📥 [Download Gopogh Report]($URL)" >> "$summary" | |
else | |
echo "Could not determine artifact ID (action version may not expose it). Find artifact named: $ARTIFACT_NAME" | |
echo "Report artifact name: $ARTIFACT_NAME" | tee -a "$summary" | |
fi | |
} | |
Print_Gopogh_Artifact_Download_URL | |
echo "-------------------- RESULT SUMMARY --------------------" | |
echo "$RESULT_SHORT" | tee -a "$summary" | |
echo "Time Elapsed: ${TIME_ELAPSED}" | tee -a "$summary" | |
numFail=$(echo "$STAT" | jq -r '.NumberOfFail // 0') | |
numPass=$(echo "$STAT" | jq -r '.NumberOfPass // 0') | |
numSkip=$(echo "$STAT" | jq -r '.NumberOfSkip // 0') | |
# Print test counts only if they are non-zero | |
print_test_counts_only() { | |
if [ -n "${numFail}" ]; then | |
echo "Failed: ${numFail}" | tee -a "$summary" | |
fi | |
if [ -n "${numPass}" ]; then | |
echo "Passed: ${numPass}" | tee -a "$summary" | |
fi | |
if [ -n "${numSkip}" ]; then | |
echo "Skipped: ${numSkip}" | tee -a "$summary" | |
fi | |
} | |
print_test_counts_only | |
# Prints lits of test names grouped by result status | |
print_test_names_by_status() { | |
local count="$1" header="$2" sym="$3" field="$4" to_summary="$5" | |
(( count > 0 )) || return 0 | |
local line="------------------------ ${count} ${header} ------------------------" | |
if [ "$to_summary" = "yes" ]; then | |
echo "$line" | tee -a "$summary" | |
jq -r ".${field}[]? | \" ${sym} \(.)\"" <<<"$STAT" | tee -a "$summary" | |
else | |
echo "$line" | |
jq -r ".${field}[]? | \" ${sym} \(.)\"" <<<"$STAT" | |
fi | |
} | |
print_test_names_by_status "${numFail:-0}" "Failed" "✗" "FailedTests" yes | |
print_test_names_by_status "${numPass:-0}" "Passed" "✓" "PassedTests" no | |
print_test_names_by_status "${numSkip:-0}" "Skipped" "•" "SkippedTests" yes | |
echo $summary >> $GITHUB_STEP_SUMMARY | |
decide_exit_code() { | |
# Allow overriding minimum expected passes for when some tests pass and others are timed out | |
local min_pass="${MIN_PASS_THRESHOLD:-45}" | |
local timeout_pattern="Test Timed out" | |
echo "---------------------------------------------------------" | |
# Timeout detection | |
if echo "$RESULT_SHORT" | grep -iq "$timeout_pattern"; then | |
echo "*** Detected test timeout ${TIME_ELAPSED} ⌛: '$timeout_pattern' ***" | |
exit 3 | |
fi | |
# Any failures | |
if [ "${numFail:-0}" -gt 0 ]; then | |
echo "*** ${numFail} test(s) failed ***" | |
exit 2 | |
fi | |
# Zero passes (likely setup issue) | |
if [ "${numPass:-0}" -eq 0 ]; then | |
echo "*** No tests passed ***" | |
exit 4 | |
fi | |
# Insufficient passes safeguard | |
if [ "${numPass:-0}" -lt "$min_pass" ]; then | |
echo "*** Only ${numPass} passed (< required ${min_pass}) ***" | tee -a "$summary" | |
exit 5 | |
fi | |
echo "Exit criteria satisfied: ${numPass} passed, ${numFail} failed, ${numSkip} skipped." | |
} | |
decide_exit_code |