19 changed files with 565 additions and 376 deletions
@ -0,0 +1,22 @@ |
|||
.git |
|||
.gitignore |
|||
.sisyphus/ |
|||
.venv/ |
|||
.pytest_cache/ |
|||
__pycache__/ |
|||
*.py[cod] |
|||
node_modules/ |
|||
instance/ |
|||
app/static/css/ |
|||
playwright-report/ |
|||
test-results/ |
|||
ui-audit-home-dark.png |
|||
ui-audit-home-default.png |
|||
ui-audit-home-sidebar-collapsed.png |
|||
ui-polish-desktop-collapsed.png |
|||
ui-polish-desktop-default.png |
|||
ui-polish-mobile-final-check.png |
|||
ui-polish-mobile-hidden-current.png |
|||
tests/ |
|||
docs/ |
|||
README.md |
|||
@ -0,0 +1,39 @@ |
|||
FROM node:20-bookworm-slim AS assets |
|||
|
|||
WORKDIR /opt/hw |
|||
|
|||
COPY package.json package-lock.json ./ |
|||
RUN npm ci |
|||
|
|||
COPY app/static/src ./app/static/src |
|||
COPY app/templates ./app/templates |
|||
RUN mkdir -p ./app/static/css && npm run build:css |
|||
|
|||
FROM python:3.13-slim-bookworm |
|||
|
|||
ENV PYTHONDONTWRITEBYTECODE=1 \ |
|||
PYTHONUNBUFFERED=1 \ |
|||
PIP_NO_CACHE_DIR=1 |
|||
|
|||
WORKDIR /opt/hw |
|||
|
|||
RUN groupadd --system --gid 10001 hw \ |
|||
&& useradd --system --uid 10001 --gid 10001 --create-home --home-dir /opt/hw hw |
|||
|
|||
COPY requirements.txt ./requirements.txt |
|||
RUN pip install -r requirements.txt |
|||
|
|||
COPY . . |
|||
COPY --from=assets /opt/hw/app/static/css/main.css /opt/hw/app/static/css/main.css |
|||
|
|||
RUN chmod +x /opt/hw/docker/entrypoint.sh \ |
|||
&& mkdir -p /opt/hw/instance/csv_previews \ |
|||
&& chown -R hw:hw /opt/hw |
|||
|
|||
USER hw |
|||
|
|||
EXPOSE 8000 |
|||
|
|||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import json, urllib.request; payload = json.load(urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)); raise SystemExit(0 if payload.get('status') == 'ok' else 1)" |
|||
|
|||
ENTRYPOINT ["/opt/hw/docker/entrypoint.sh"] |
|||
@ -1,13 +0,0 @@ |
|||
example.com { |
|||
encode zstd gzip |
|||
|
|||
log { |
|||
output stderr |
|||
} |
|||
|
|||
reverse_proxy 127.0.0.1:8000 { |
|||
header_up X-Forwarded-For {remote_host} |
|||
header_up X-Forwarded-Proto {scheme} |
|||
header_up Host {host} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
[Unit] |
|||
Description=HappyWedding Gunicorn Service |
|||
After=network.target |
|||
|
|||
[Service] |
|||
Type=simple |
|||
User=hw |
|||
Group=hw |
|||
WorkingDirectory=/opt/hw |
|||
EnvironmentFile=/etc/hw/hw.env |
|||
ExecStart=/opt/hw/.venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 run:app |
|||
Restart=on-failure |
|||
RestartSec=5 |
|||
|
|||
[Install] |
|||
WantedBy=multi-user.target |
|||
@ -0,0 +1,34 @@ |
|||
#!/bin/sh |
|||
set -eu |
|||
|
|||
APP_DIR="${APP_DIR:-/opt/hw}" |
|||
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}" |
|||
ENV_FILE="${ENV_FILE:-${CONFIG_DIR}/hw.env}" |
|||
INSTANCE_DIR="${INSTANCE_DIR:-${APP_DIR}/instance}" |
|||
SEED_MARKER_FILE="${SEED_MARKER_FILE:-${INSTANCE_DIR}/.seed-all-complete}" |
|||
|
|||
if [ -f "$ENV_FILE" ]; then |
|||
set -a |
|||
# shellcheck disable=SC1090 |
|||
. "$ENV_FILE" |
|||
set +a |
|||
fi |
|||
|
|||
export FLASK_APP="${FLASK_APP:-run.py}" |
|||
export HW_CONFIG="${HW_CONFIG:-production}" |
|||
export PYTHONPATH="${PYTHONPATH:-${APP_DIR}}" |
|||
|
|||
mkdir -p "$INSTANCE_DIR/csv_previews" |
|||
|
|||
python -m flask db upgrade |
|||
|
|||
if [ "${RUN_SEED_ALL:-0}" = "1" ] && [ ! -f "$SEED_MARKER_FILE" ]; then |
|||
python -m flask seed-all |
|||
: > "$SEED_MARKER_FILE" |
|||
fi |
|||
|
|||
if [ "$#" -eq 0 ]; then |
|||
set -- gunicorn --workers "${HW_GUNICORN_WORKERS:-2}" --bind "0.0.0.0:${HW_GUNICORN_PORT:-8000}" run:app |
|||
fi |
|||
|
|||
exec "$@" |
|||
@ -0,0 +1,81 @@ |
|||
#!/usr/bin/env bash |
|||
set -euo pipefail |
|||
|
|||
APP_DIR="${APP_DIR:-/opt/hw}" |
|||
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}" |
|||
ENV_FILE="${ENV_FILE:-$CONFIG_DIR/hw.env}" |
|||
CONTAINER_NAME="${CONTAINER_NAME:-hw}" |
|||
IMAGE_NAME="${IMAGE_NAME:-hw}" |
|||
IMAGE_TAG="${IMAGE_TAG:-py313}" |
|||
APP_PORT="${APP_PORT:-8000}" |
|||
RUN_SEED_ALL="${RUN_SEED_ALL:-0}" |
|||
HOST_PORT="${HOST_PORT:-8000}" |
|||
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}" |
|||
CONTAINER_UID="${CONTAINER_UID:-10001}" |
|||
CONTAINER_GID="${CONTAINER_GID:-10001}" |
|||
|
|||
require_root() { |
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
return |
|||
fi |
|||
if [[ "${EUID}" -ne 0 ]]; then |
|||
printf 'Please run this script as root.\n' >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
require_file() { |
|||
local path="$1" |
|||
if [[ ! -f "$path" ]]; then |
|||
printf 'Required file not found: %s\n' "$path" >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
require_docker_daemon() { |
|||
if ! docker info >/dev/null 2>&1; then |
|||
printf 'Docker daemon is not available. Please start Docker and re-run this script.\n' >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
main() { |
|||
require_root |
|||
require_file "$ENV_FILE" |
|||
require_file "$APP_DIR/Dockerfile" |
|||
require_file "$APP_DIR/run.py" |
|||
require_docker_daemon |
|||
|
|||
install -d -m 755 "$APP_DIR/instance" |
|||
install -d -m 755 "$APP_DIR/instance/csv_previews" |
|||
|
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" >/dev/null 2>&1 || true |
|||
else |
|||
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" |
|||
fi |
|||
|
|||
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" "$APP_DIR" |
|||
|
|||
if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then |
|||
docker rm -f "$CONTAINER_NAME" >/dev/null |
|||
fi |
|||
|
|||
docker run -d \ |
|||
--name "$CONTAINER_NAME" \ |
|||
--env-file "$ENV_FILE" \ |
|||
-e RUN_SEED_ALL="$RUN_SEED_ALL" \ |
|||
-e HW_GUNICORN_PORT="$APP_PORT" \ |
|||
-p "$HOST_PORT:$APP_PORT" \ |
|||
-v "$APP_DIR/instance:/opt/hw/instance" \ |
|||
-v "$CONFIG_DIR:/opt/hw.conf.d:ro" \ |
|||
--restart unless-stopped \ |
|||
"${IMAGE_NAME}:${IMAGE_TAG}" |
|||
|
|||
printf 'Container deployment completed.\n' |
|||
printf 'Container: %s\n' "$CONTAINER_NAME" |
|||
printf 'Image: %s:%s\n' "$IMAGE_NAME" "$IMAGE_TAG" |
|||
printf 'Published port: %s -> %s\n' "$HOST_PORT" "$APP_PORT" |
|||
} |
|||
|
|||
main "$@" |
|||
@ -1,55 +0,0 @@ |
|||
#!/usr/bin/env bash |
|||
set -euo pipefail |
|||
|
|||
APP_DIR="${APP_DIR:-/opt/hw}" |
|||
ENV_FILE="${ENV_FILE:-/etc/hw/hw.env}" |
|||
SERVICE_NAME="${SERVICE_NAME:-hw}" |
|||
RUN_SEED_ALL="${RUN_SEED_ALL:-0}" |
|||
|
|||
require_root() { |
|||
if [[ "${EUID}" -ne 0 ]]; then |
|||
printf 'Please run this script as root.\n' >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
require_file() { |
|||
local path="$1" |
|||
if [[ ! -f "$path" ]]; then |
|||
printf 'Required file not found: %s\n' "$path" >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
main() { |
|||
require_root |
|||
require_file "$ENV_FILE" |
|||
require_file "$APP_DIR/requirements.txt" |
|||
require_file "$APP_DIR/run.py" |
|||
|
|||
set -a |
|||
# shellcheck disable=SC1090 |
|||
source "$ENV_FILE" |
|||
set +a |
|||
|
|||
"$APP_DIR/.venv/bin/python" -m pip install --upgrade pip |
|||
"$APP_DIR/.venv/bin/python" -m pip install -r "$APP_DIR/requirements.txt" |
|||
|
|||
export FLASK_APP=run.py |
|||
export HW_CONFIG="${HW_CONFIG:-production}" |
|||
|
|||
(cd "$APP_DIR" && "$APP_DIR/.venv/bin/python" -m flask db upgrade) |
|||
|
|||
if [[ "$RUN_SEED_ALL" == "1" ]]; then |
|||
(cd "$APP_DIR" && "$APP_DIR/.venv/bin/python" -m flask seed-all) |
|||
fi |
|||
|
|||
systemctl daemon-reload |
|||
systemctl restart "$SERVICE_NAME" |
|||
systemctl enable "$SERVICE_NAME" |
|||
|
|||
printf 'Deployment completed.\n' |
|||
printf 'Use: systemctl status %s --no-pager\n' "$SERVICE_NAME" |
|||
} |
|||
|
|||
main "$@" |
|||
@ -0,0 +1,101 @@ |
|||
#!/usr/bin/env bash |
|||
set -euo pipefail |
|||
|
|||
APP_DIR="${APP_DIR:-/opt/hw}" |
|||
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}" |
|||
DEFAULT_HOST_USER="${SUDO_USER:-$(id -un)}" |
|||
APP_USER="${APP_USER:-$DEFAULT_HOST_USER}" |
|||
APP_GROUP="${APP_GROUP:-$(id -gn "$APP_USER" 2>/dev/null || id -gn)}" |
|||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
|||
ENV_TEMPLATE_SOURCE="${ENV_TEMPLATE_SOURCE:-$PROJECT_ROOT/deploy/env/hw.env.example}" |
|||
ENV_FILE_NAME="${ENV_FILE_NAME:-hw.env}" |
|||
DOCKER_INSTALL_SCRIPT_URL="https://get.docker.com" |
|||
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}" |
|||
CONTAINER_UID="${CONTAINER_UID:-10001}" |
|||
CONTAINER_GID="${CONTAINER_GID:-10001}" |
|||
|
|||
require_root() { |
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
return |
|||
fi |
|||
if [[ "${EUID}" -ne 0 ]]; then |
|||
printf 'Please run this script as root.\n' >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
command_exists() { |
|||
command -v "$1" >/dev/null 2>&1 |
|||
} |
|||
|
|||
require_command() { |
|||
if ! command_exists "$1"; then |
|||
printf 'Required command not found: %s\n' "$1" >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
install_docker_if_missing() { |
|||
if command_exists docker; then |
|||
return |
|||
fi |
|||
|
|||
if command_exists apt-get; then |
|||
printf 'Docker not found. Installing Docker via %s ...\n' "$DOCKER_INSTALL_SCRIPT_URL" |
|||
curl -fsSL "$DOCKER_INSTALL_SCRIPT_URL" | sh |
|||
return |
|||
fi |
|||
|
|||
printf 'Docker is not installed and automatic installation is only supported on apt-based hosts.\n' >&2 |
|||
printf 'Please install Docker manually, then re-run this script.\n' >&2 |
|||
exit 1 |
|||
} |
|||
|
|||
main() { |
|||
require_root |
|||
require_command curl |
|||
install_docker_if_missing |
|||
|
|||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR" |
|||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance" |
|||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance/csv_previews" |
|||
|
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
install -d -m 750 "$CONFIG_DIR" |
|||
else |
|||
install -d -m 750 -o root -g "$APP_GROUP" "$CONFIG_DIR" |
|||
fi |
|||
|
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" >/dev/null 2>&1 || true |
|||
else |
|||
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" |
|||
fi |
|||
|
|||
if [[ ! -f "$CONFIG_DIR/$ENV_FILE_NAME" ]]; then |
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
install -m 640 "$ENV_TEMPLATE_SOURCE" "$CONFIG_DIR/$ENV_FILE_NAME" |
|||
else |
|||
install -m 640 -o root -g "$APP_GROUP" "$ENV_TEMPLATE_SOURCE" "$CONFIG_DIR/$ENV_FILE_NAME" |
|||
fi |
|||
printf 'Created %s from template. Please edit it before deployment.\n' "$CONFIG_DIR/$ENV_FILE_NAME" |
|||
else |
|||
printf 'Keeping existing %s\n' "$CONFIG_DIR/$ENV_FILE_NAME" |
|||
fi |
|||
|
|||
if [[ "$ALLOW_NON_ROOT" != "1" ]] && id -u "$APP_USER" >/dev/null 2>&1; then |
|||
usermod -aG docker "$APP_USER" >/dev/null 2>&1 || true |
|||
fi |
|||
|
|||
printf '\nContainer deployment prerequisites are ready.\n' |
|||
printf 'Next steps:\n' |
|||
printf '1. Sync project files into %s\n' "$APP_DIR" |
|||
printf '2. Edit %s/%s with real production values\n' "$CONFIG_DIR" "$ENV_FILE_NAME" |
|||
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
|||
printf '3. Run ALLOW_NON_ROOT=1 bash scripts/deploy-container.sh\n' |
|||
else |
|||
printf '3. Run sudo bash scripts/deploy-container.sh\n' |
|||
fi |
|||
} |
|||
|
|||
main "$@" |
|||
@ -1,55 +0,0 @@ |
|||
#!/usr/bin/env bash |
|||
set -euo pipefail |
|||
|
|||
APP_USER="${APP_USER:-hw}" |
|||
APP_GROUP="${APP_GROUP:-$APP_USER}" |
|||
APP_DIR="${APP_DIR:-/opt/hw}" |
|||
ENV_DIR="${ENV_DIR:-/etc/hw}" |
|||
SERVICE_NAME="${SERVICE_NAME:-hw}" |
|||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
|||
|
|||
require_root() { |
|||
if [[ "${EUID}" -ne 0 ]]; then |
|||
printf 'Please run this script as root.\n' >&2 |
|||
exit 1 |
|||
fi |
|||
} |
|||
|
|||
ensure_user() { |
|||
if ! id -u "$APP_USER" >/dev/null 2>&1; then |
|||
useradd --system --create-home --shell /usr/sbin/nologin "$APP_USER" |
|||
fi |
|||
} |
|||
|
|||
main() { |
|||
require_root |
|||
ensure_user |
|||
|
|||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR" |
|||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance" |
|||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance/csv_previews" |
|||
install -d -m 750 -o root -g "$APP_GROUP" "$ENV_DIR" |
|||
|
|||
if [[ ! -d "$APP_DIR/.venv" ]]; then |
|||
python3 -m venv "$APP_DIR/.venv" |
|||
fi |
|||
|
|||
"$APP_DIR/.venv/bin/python" -m pip install --upgrade pip |
|||
|
|||
install -m 644 "$PROJECT_ROOT/deploy/systemd/hw.service" "/etc/systemd/system/${SERVICE_NAME}.service" |
|||
|
|||
if [[ ! -f "$ENV_DIR/hw.env" ]]; then |
|||
install -m 640 -o root -g "$APP_GROUP" "$PROJECT_ROOT/deploy/env/hw.env.example" "$ENV_DIR/hw.env" |
|||
printf 'Created %s/hw.env from template. Please edit it before starting the service.\n' "$ENV_DIR" |
|||
else |
|||
printf 'Keeping existing %s/hw.env\n' "$ENV_DIR" |
|||
fi |
|||
|
|||
printf '\nNext steps:\n' |
|||
printf '1. Sync project files into %s\n' "$APP_DIR" |
|||
printf '2. Edit %s/hw.env with real production values\n' "$ENV_DIR" |
|||
printf '3. Copy deploy/caddy/Caddyfile into your Caddy config and replace the domain\n' |
|||
printf '4. Run scripts/deploy-systemd.sh as root\n' |
|||
} |
|||
|
|||
main "$@" |
|||
@ -0,0 +1,72 @@ |
|||
#!/usr/bin/env bash |
|||
set -euo pipefail |
|||
|
|||
CONTAINER_NAME="${CONTAINER_NAME:-hw}" |
|||
HOST_PORT="${HOST_PORT:-${APP_PORT:-8000}}" |
|||
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1:${HOST_PORT}/health}" |
|||
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}" |
|||
|
|||
fail() { |
|||
printf 'Verification failed: %s\n' "$1" >&2 |
|||
exit 1 |
|||
} |
|||
|
|||
command_exists() { |
|||
command -v "$1" >/dev/null 2>&1 |
|||
} |
|||
|
|||
require_docker_daemon() { |
|||
if ! docker info >/dev/null 2>&1; then |
|||
fail "docker daemon is not available" |
|||
fi |
|||
} |
|||
|
|||
port_is_listening() { |
|||
local port="$1" |
|||
|
|||
if command_exists ss; then |
|||
ss -tln | grep -q ":${port} " |
|||
return |
|||
fi |
|||
|
|||
if command_exists lsof; then |
|||
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 |
|||
return |
|||
fi |
|||
|
|||
if command_exists netstat; then |
|||
netstat -an | grep -E -q "[\.:]${port}[[:space:]].*LISTEN" |
|||
return |
|||
fi |
|||
|
|||
fail "no supported port inspection command found (need one of: ss, lsof, netstat)" |
|||
} |
|||
|
|||
main() { |
|||
require_docker_daemon |
|||
if ! docker ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then |
|||
fail "container '$CONTAINER_NAME' is not running" |
|||
fi |
|||
|
|||
health_status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CONTAINER_NAME")" |
|||
if [[ "$health_status" != "healthy" && "$health_status" != "none" ]]; then |
|||
fail "container health status is '$health_status'" |
|||
fi |
|||
|
|||
if ! port_is_listening "$HOST_PORT"; then |
|||
fail "nothing is listening on TCP port ${HOST_PORT}" |
|||
fi |
|||
|
|||
health_payload="$(curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL")" || fail "healthcheck request failed: $HEALTHCHECK_URL" |
|||
|
|||
if ! printf '%s' "$health_payload" | grep -q '"status":"ok"\|"status": "ok"'; then |
|||
fail "healthcheck response does not contain status=ok" |
|||
fi |
|||
|
|||
printf 'Verification succeeded.\n' |
|||
printf 'Container: %s\n' "$CONTAINER_NAME" |
|||
printf 'Port: %s\n' "$HOST_PORT" |
|||
printf 'Healthcheck: %s\n' "$HEALTHCHECK_URL" |
|||
} |
|||
|
|||
main "$@" |
|||
@ -1,34 +0,0 @@ |
|||
#!/usr/bin/env bash |
|||
set -euo pipefail |
|||
|
|||
SERVICE_NAME="${SERVICE_NAME:-hw}" |
|||
APP_PORT="${APP_PORT:-8000}" |
|||
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1/health}" |
|||
|
|||
fail() { |
|||
printf 'Verification failed: %s\n' "$1" >&2 |
|||
exit 1 |
|||
} |
|||
|
|||
main() { |
|||
if ! systemctl is-active --quiet "$SERVICE_NAME"; then |
|||
fail "systemd service '$SERVICE_NAME' is not active" |
|||
fi |
|||
|
|||
if ! ss -tln | grep -q ":${APP_PORT} "; then |
|||
fail "nothing is listening on TCP port ${APP_PORT}" |
|||
fi |
|||
|
|||
health_payload="$(curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL")" || fail "healthcheck request failed: $HEALTHCHECK_URL" |
|||
|
|||
if ! printf '%s' "$health_payload" | grep -q '"status":"ok"\|"status": "ok"'; then |
|||
fail "healthcheck response does not contain status=ok" |
|||
fi |
|||
|
|||
printf 'Verification succeeded.\n' |
|||
printf 'Service: %s\n' "$SERVICE_NAME" |
|||
printf 'Port: %s\n' "$APP_PORT" |
|||
printf 'Healthcheck: %s\n' "$HEALTHCHECK_URL" |
|||
} |
|||
|
|||
main "$@" |
|||
Loading…
Reference in new issue